fastapi-cloud-cli 0.17.0__tar.gz → 0.18.0__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.
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/PKG-INFO +2 -1
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/pyproject.toml +10 -2
- fastapi_cloud_cli-0.18.0/src/fastapi_cloud_cli/__init__.py +1 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/deploy.py +65 -10
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/env.py +12 -3
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/login.py +24 -22
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/logs.py +4 -1
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/setup_ci.py +4 -1
- fastapi_cloud_cli-0.18.0/src/fastapi_cloud_cli/commands/whoami.py +37 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/api.py +20 -1
- fastapi_cloud_cli-0.18.0/src/fastapi_cloud_cli/utils/cli.py +128 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/config.py +4 -0
- fastapi_cloud_cli-0.18.0/src/fastapi_cloud_cli/utils/version_check.py +187 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/conftest.py +1 -0
- fastapi_cloud_cli-0.18.0/tests/test_cli.py +84 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_deploy.py +188 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_deploy_utils.py +52 -0
- fastapi_cloud_cli-0.18.0/tests/test_version_check.py +381 -0
- fastapi_cloud_cli-0.17.0/src/fastapi_cloud_cli/__init__.py +0 -1
- fastapi_cloud_cli-0.17.0/src/fastapi_cloud_cli/commands/whoami.py +0 -33
- fastapi_cloud_cli-0.17.0/src/fastapi_cloud_cli/utils/cli.py +0 -68
- fastapi_cloud_cli-0.17.0/tests/test_cli.py +0 -37
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/README.md +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/scripts/add_latest_release_date.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/cli.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/link.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/logging.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/progress_file.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_api_client.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_archive.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_auth.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_link.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_login.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_setup_ci.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_unlink.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_cli_whoami.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_config.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_env_delete.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_env_list.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_env_set.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_logs.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_progress_file.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/test_sentry.py +0 -0
- {fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-cloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Deploy and manage FastAPI Cloud apps from the command line 🚀
|
|
5
5
|
Author-Email: Patrick Arminio <patrick@fastapilabs.com>
|
|
6
6
|
License: MIT
|
|
@@ -39,6 +39,7 @@ Requires-Dist: pydantic[email]>=2.8.0; python_version == "3.13"
|
|
|
39
39
|
Requires-Dist: pydantic[email]>=2.12.0; python_version >= "3.14"
|
|
40
40
|
Requires-Dist: sentry-sdk>=2.20.0
|
|
41
41
|
Requires-Dist: fastar>=0.10.0
|
|
42
|
+
Requires-Dist: detect-installer>=0.1.0
|
|
42
43
|
Provides-Extra: standard
|
|
43
44
|
Requires-Dist: uvicorn[standard]>=0.15.0; extra == "standard"
|
|
44
45
|
Description-Content-Type: text/markdown
|
|
@@ -39,8 +39,9 @@ dependencies = [
|
|
|
39
39
|
"pydantic[email] >= 2.12.0; python_version >= '3.14'",
|
|
40
40
|
"sentry-sdk >= 2.20.0",
|
|
41
41
|
"fastar >= 0.10.0",
|
|
42
|
+
"detect-installer>=0.1.0",
|
|
42
43
|
]
|
|
43
|
-
version = "0.
|
|
44
|
+
version = "0.18.0"
|
|
44
45
|
|
|
45
46
|
[project.license]
|
|
46
47
|
text = "MIT"
|
|
@@ -66,7 +67,11 @@ dev = [
|
|
|
66
67
|
"ruff==0.13.0",
|
|
67
68
|
"respx==0.22.0",
|
|
68
69
|
"time-machine==2.15.0",
|
|
69
|
-
"ty>=0.0.
|
|
70
|
+
"ty>=0.0.25",
|
|
71
|
+
"zizmor>=1.24.1",
|
|
72
|
+
]
|
|
73
|
+
github-actions = [
|
|
74
|
+
"smokeshow>=0.5.0",
|
|
70
75
|
]
|
|
71
76
|
|
|
72
77
|
[build-system]
|
|
@@ -134,6 +139,9 @@ exclude = [
|
|
|
134
139
|
"tests/assets/**",
|
|
135
140
|
]
|
|
136
141
|
|
|
142
|
+
[tool.ty.terminal]
|
|
143
|
+
error-on-warning = true
|
|
144
|
+
|
|
137
145
|
[tool.ruff.lint]
|
|
138
146
|
select = [
|
|
139
147
|
"E",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.18.0"
|
{fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/deploy.py
RENAMED
|
@@ -108,15 +108,19 @@ def _should_exclude_entry(path: Path) -> bool:
|
|
|
108
108
|
return False
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
files = rignore.walk(
|
|
111
|
+
def _rignore_walk(path: Path) -> rignore.Walker:
|
|
112
|
+
return rignore.walk(
|
|
114
113
|
path,
|
|
115
114
|
should_exclude_entry=_should_exclude_entry,
|
|
116
115
|
additional_ignore_paths=[".fastapicloudignore"],
|
|
117
116
|
ignore_hidden=False,
|
|
118
117
|
)
|
|
119
118
|
|
|
119
|
+
|
|
120
|
+
def archive(path: Path, tar_path: Path) -> Path:
|
|
121
|
+
logger.debug("Starting archive creation for path: %s", path)
|
|
122
|
+
files = _rignore_walk(path)
|
|
123
|
+
|
|
120
124
|
logger.debug("Archive will be created at: %s", tar_path)
|
|
121
125
|
|
|
122
126
|
file_count = 0
|
|
@@ -134,6 +138,20 @@ def archive(path: Path, tar_path: Path) -> Path:
|
|
|
134
138
|
return tar_path
|
|
135
139
|
|
|
136
140
|
|
|
141
|
+
def _get_large_files(path: Path, threshold_mb: int) -> list[tuple[Path, int]]:
|
|
142
|
+
threshold_bytes = threshold_mb * 1024 * 1024
|
|
143
|
+
large_files = []
|
|
144
|
+
files = _rignore_walk(path)
|
|
145
|
+
for filename in files:
|
|
146
|
+
if filename.is_dir():
|
|
147
|
+
continue
|
|
148
|
+
file_size = filename.stat().st_size
|
|
149
|
+
if file_size > threshold_bytes:
|
|
150
|
+
large_files.append((filename.relative_to(path), file_size))
|
|
151
|
+
|
|
152
|
+
return sorted(large_files, key=lambda x: x[1], reverse=True)
|
|
153
|
+
|
|
154
|
+
|
|
137
155
|
class Team(BaseModel):
|
|
138
156
|
id: str
|
|
139
157
|
slug: str
|
|
@@ -263,8 +281,8 @@ def _upload_deployment(
|
|
|
263
281
|
logger.debug("Upload notification sent successfully")
|
|
264
282
|
|
|
265
283
|
|
|
266
|
-
def _get_app(client: APIClient,
|
|
267
|
-
response = client.get(f"/apps/{
|
|
284
|
+
def _get_app(client: APIClient, app_id: str) -> AppResponse | None:
|
|
285
|
+
response = client.get(f"/apps/{app_id}")
|
|
268
286
|
|
|
269
287
|
if response.status_code == 404:
|
|
270
288
|
return None
|
|
@@ -378,10 +396,14 @@ def _configure_app(
|
|
|
378
396
|
initial_directory = selected_app.directory if selected_app else ""
|
|
379
397
|
|
|
380
398
|
directory_input = toolkit.input(
|
|
381
|
-
title=
|
|
399
|
+
title=(
|
|
400
|
+
"Directory where your app's pyproject.toml file lives (e.g. src, backend):"
|
|
401
|
+
),
|
|
382
402
|
tag="dir",
|
|
383
403
|
value=initial_directory or "",
|
|
384
|
-
placeholder=
|
|
404
|
+
placeholder=(
|
|
405
|
+
"[italic]Leave empty if pyproject.toml is in the current directory[/italic]"
|
|
406
|
+
),
|
|
385
407
|
validator=TypeAdapter(AppDirectory),
|
|
386
408
|
)
|
|
387
409
|
|
|
@@ -665,7 +687,10 @@ def deploy(
|
|
|
665
687
|
path: Annotated[
|
|
666
688
|
Path | None,
|
|
667
689
|
typer.Argument(
|
|
668
|
-
help=
|
|
690
|
+
help=(
|
|
691
|
+
"Path to the directory with your app's pyproject.toml "
|
|
692
|
+
"(defaults to current directory)"
|
|
693
|
+
)
|
|
669
694
|
),
|
|
670
695
|
] = None,
|
|
671
696
|
skip_wait: Annotated[
|
|
@@ -679,6 +704,14 @@ def deploy(
|
|
|
679
704
|
envvar="FASTAPI_CLOUD_APP_ID",
|
|
680
705
|
),
|
|
681
706
|
] = None,
|
|
707
|
+
large_file_threshold: Annotated[
|
|
708
|
+
int,
|
|
709
|
+
typer.Option(
|
|
710
|
+
help="File size threshold in MB for warning about large files",
|
|
711
|
+
min=1,
|
|
712
|
+
envvar="FASTAPI_CLOUD_LARGE_FILE_THRESHOLD",
|
|
713
|
+
),
|
|
714
|
+
] = 10, # 10 MB
|
|
682
715
|
) -> Any:
|
|
683
716
|
"""
|
|
684
717
|
Deploy a [bold]FastAPI[/bold] app to FastAPI Cloud. 🚀
|
|
@@ -786,7 +819,7 @@ def deploy(
|
|
|
786
819
|
with toolkit.progress("Checking app...", transient=True) as progress:
|
|
787
820
|
with client.handle_http_errors(progress):
|
|
788
821
|
logger.debug("Checking app with ID: %s", target_app_id)
|
|
789
|
-
app = _get_app(client=client,
|
|
822
|
+
app = _get_app(client=client, app_id=target_app_id)
|
|
790
823
|
|
|
791
824
|
if not app:
|
|
792
825
|
logger.debug("App not found in API")
|
|
@@ -804,10 +837,32 @@ def deploy(
|
|
|
804
837
|
)
|
|
805
838
|
raise typer.Exit(1)
|
|
806
839
|
|
|
840
|
+
large_files = _get_large_files(
|
|
841
|
+
path_to_deploy, threshold_mb=large_file_threshold
|
|
842
|
+
)
|
|
843
|
+
if large_files:
|
|
844
|
+
toolkit.print(
|
|
845
|
+
f"⚠️ Some uploaded files are larger than {large_file_threshold} MB ⚖️ :",
|
|
846
|
+
tag="warning",
|
|
847
|
+
)
|
|
848
|
+
for fname, fsize in large_files[:3]:
|
|
849
|
+
fsize_mb = fsize // (1024 * 1024)
|
|
850
|
+
toolkit.print(f" • {fname} [yellow]({fsize_mb} MB)[/yellow]")
|
|
851
|
+
is_more = len(large_files) > 3
|
|
852
|
+
if is_more:
|
|
853
|
+
toolkit.print(f" [dim]...and {len(large_files) - 3} more[/dim]")
|
|
854
|
+
|
|
855
|
+
large_files_docs_url = "https://fastapicloud.com/docs/fastapi-cloud-cli/deploy/#large-files-warning"
|
|
856
|
+
toolkit.print(
|
|
857
|
+
f"Read more: [link={large_files_docs_url}]{large_files_docs_url}[/link]",
|
|
858
|
+
tag="tip",
|
|
859
|
+
)
|
|
860
|
+
toolkit.print_line()
|
|
861
|
+
|
|
807
862
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
808
863
|
logger.debug("Creating archive for deployment")
|
|
809
864
|
archive_path = Path(temp_dir) / "archive.tar"
|
|
810
|
-
archive(
|
|
865
|
+
archive(path_to_deploy, archive_path)
|
|
811
866
|
|
|
812
867
|
with (
|
|
813
868
|
toolkit.progress(
|
|
@@ -61,7 +61,10 @@ def list(
|
|
|
61
61
|
path: Annotated[
|
|
62
62
|
Path | None,
|
|
63
63
|
typer.Argument(
|
|
64
|
-
help=
|
|
64
|
+
help=(
|
|
65
|
+
"Path to the directory with your app's pyproject.toml "
|
|
66
|
+
"(defaults to current directory)"
|
|
67
|
+
)
|
|
65
68
|
),
|
|
66
69
|
] = None,
|
|
67
70
|
) -> Any:
|
|
@@ -119,7 +122,10 @@ def delete(
|
|
|
119
122
|
path: Annotated[
|
|
120
123
|
Path | None,
|
|
121
124
|
typer.Argument(
|
|
122
|
-
help=
|
|
125
|
+
help=(
|
|
126
|
+
"Path to the directory with your app's pyproject.toml "
|
|
127
|
+
"(defaults to current directory)"
|
|
128
|
+
)
|
|
123
129
|
),
|
|
124
130
|
] = None,
|
|
125
131
|
) -> Any:
|
|
@@ -208,7 +214,10 @@ def set(
|
|
|
208
214
|
path: Annotated[
|
|
209
215
|
Path | None,
|
|
210
216
|
typer.Argument(
|
|
211
|
-
help=
|
|
217
|
+
help=(
|
|
218
|
+
"Path to the directory with your app's pyproject.toml "
|
|
219
|
+
"(defaults to current directory)"
|
|
220
|
+
)
|
|
212
221
|
),
|
|
213
222
|
] = None,
|
|
214
223
|
secret: Annotated[
|
{fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/login.py
RENAMED
|
@@ -77,18 +77,18 @@ def login() -> Any:
|
|
|
77
77
|
Login to FastAPI Cloud. 🚀
|
|
78
78
|
"""
|
|
79
79
|
identity = Identity()
|
|
80
|
+
is_logged_in = identity.is_logged_in()
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
with get_rich_toolkit(minimal=is_logged_in) as toolkit:
|
|
83
|
+
if is_logged_in:
|
|
83
84
|
toolkit.print("You are already logged in.")
|
|
84
85
|
toolkit.print(
|
|
85
86
|
"Run [bold]fastapi cloud logout[/bold] first if you want to switch accounts."
|
|
86
87
|
)
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
return
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
with get_rich_toolkit() as toolkit:
|
|
91
|
+
if identity.has_deploy_token():
|
|
92
92
|
toolkit.print(
|
|
93
93
|
"You have [bold blue]FASTAPI_CLOUD_TOKEN[/] environment variable set.\n"
|
|
94
94
|
"This token will take precedence over the user token for "
|
|
@@ -96,29 +96,31 @@ def login() -> Any:
|
|
|
96
96
|
tag="Warning",
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
with APIClient() as client:
|
|
100
|
+
toolkit.print_title("Login to FastAPI Cloud", tag="FastAPI")
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
toolkit.print_line()
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
with toolkit.progress("Starting authorization") as progress:
|
|
105
|
+
with client.handle_http_errors(progress):
|
|
106
|
+
authorization_data = _start_device_authorization(client)
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
url = authorization_data.verification_uri_complete
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
progress.log(f"Opening [link={url}]{url}[/link]")
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
toolkit.print_line()
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
with toolkit.progress("Waiting for user to authorize...") as progress:
|
|
115
|
+
typer.launch(url)
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
with client.handle_http_errors(progress):
|
|
118
|
+
access_token = _fetch_access_token(
|
|
119
|
+
client,
|
|
120
|
+
authorization_data.device_code,
|
|
121
|
+
authorization_data.interval,
|
|
122
|
+
)
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
write_auth_config(AuthConfig(access_token=access_token))
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
progress.log("Now you are logged in! 🚀")
|
{fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/logs.py
RENAMED
|
@@ -113,7 +113,10 @@ def logs(
|
|
|
113
113
|
path: Annotated[
|
|
114
114
|
Path | None,
|
|
115
115
|
typer.Argument(
|
|
116
|
-
help=
|
|
116
|
+
help=(
|
|
117
|
+
"Path to the directory with your app's pyproject.toml "
|
|
118
|
+
"(defaults to current directory)"
|
|
119
|
+
)
|
|
117
120
|
),
|
|
118
121
|
] = None,
|
|
119
122
|
tail: int = typer.Option(
|
{fastapi_cloud_cli-0.17.0 → fastapi_cloud_cli-0.18.0}/src/fastapi_cloud_cli/commands/setup_ci.py
RENAMED
|
@@ -157,7 +157,10 @@ def setup_ci(
|
|
|
157
157
|
path: Annotated[
|
|
158
158
|
Path | None,
|
|
159
159
|
typer.Argument(
|
|
160
|
-
help=
|
|
160
|
+
help=(
|
|
161
|
+
"Path to the directory with your app's pyproject.toml "
|
|
162
|
+
"(defaults to current directory)"
|
|
163
|
+
)
|
|
161
164
|
),
|
|
162
165
|
] = None,
|
|
163
166
|
branch: str | None = typer.Option(
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fastapi_cloud_cli.utils.api import APIClient
|
|
5
|
+
from fastapi_cloud_cli.utils.auth import Identity
|
|
6
|
+
from fastapi_cloud_cli.utils.cli import get_rich_toolkit
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def whoami() -> Any:
|
|
12
|
+
identity = Identity()
|
|
13
|
+
|
|
14
|
+
with get_rich_toolkit(minimal=True) as toolkit:
|
|
15
|
+
if not identity.is_logged_in():
|
|
16
|
+
toolkit.print(
|
|
17
|
+
"No credentials found. Use [blue]`fastapi login`[/] to login."
|
|
18
|
+
)
|
|
19
|
+
else:
|
|
20
|
+
with APIClient() as client:
|
|
21
|
+
with toolkit.progress(
|
|
22
|
+
title="⚡ Fetching profile",
|
|
23
|
+
transient=True,
|
|
24
|
+
) as progress:
|
|
25
|
+
with client.handle_http_errors(progress, default_message=""):
|
|
26
|
+
response = client.get("/users/me")
|
|
27
|
+
response.raise_for_status()
|
|
28
|
+
|
|
29
|
+
data = response.json()
|
|
30
|
+
|
|
31
|
+
toolkit.print(f"⚡ [bold]{data['email']}[/bold]")
|
|
32
|
+
|
|
33
|
+
if identity.has_deploy_token():
|
|
34
|
+
toolkit.print(
|
|
35
|
+
"⚡ [bold]Using API token from environment variable for "
|
|
36
|
+
"[blue]`fastapi deploy`[/blue] command.[/bold]"
|
|
37
|
+
)
|
|
@@ -209,6 +209,22 @@ def _handle_unauthorized(auth_mode: AuthMode) -> str:
|
|
|
209
209
|
return message
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
def _get_response_error_message(response: httpx.Response) -> str | None:
|
|
213
|
+
try:
|
|
214
|
+
data = response.json()
|
|
215
|
+
except (json.JSONDecodeError, httpx.ResponseNotRead):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
if not isinstance(data, dict):
|
|
219
|
+
return None # pragma: no cover
|
|
220
|
+
|
|
221
|
+
detail = data.get("detail")
|
|
222
|
+
if not isinstance(detail, str):
|
|
223
|
+
return None # pragma: no cover
|
|
224
|
+
|
|
225
|
+
return detail
|
|
226
|
+
|
|
227
|
+
|
|
212
228
|
def handle_http_error(
|
|
213
229
|
error: httpx.HTTPError,
|
|
214
230
|
default_message: str | None = None,
|
|
@@ -227,7 +243,10 @@ def handle_http_error(
|
|
|
227
243
|
message = _handle_unauthorized(auth_mode=auth_mode)
|
|
228
244
|
|
|
229
245
|
elif status_code == 403:
|
|
230
|
-
message =
|
|
246
|
+
message = (
|
|
247
|
+
_get_response_error_message(error.response)
|
|
248
|
+
or "You don't have permissions for this resource"
|
|
249
|
+
)
|
|
231
250
|
|
|
232
251
|
if not message:
|
|
233
252
|
message = (
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from rich.segment import Segment
|
|
7
|
+
from rich.style import Style
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from rich_toolkit import RichToolkit, RichToolkitTheme
|
|
10
|
+
from rich_toolkit.styles import BaseStyle, MinimalStyle, TaggedStyle
|
|
11
|
+
|
|
12
|
+
from fastapi_cloud_cli.utils.version_check import (
|
|
13
|
+
DISABLE_VERSION_CHECK_ENV,
|
|
14
|
+
BackgroundVersionCheck,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FastAPIStyle(TaggedStyle):
|
|
21
|
+
def __init__(self, tag_width: int = 11):
|
|
22
|
+
super().__init__(tag_width=tag_width)
|
|
23
|
+
|
|
24
|
+
def _get_tag_segments(
|
|
25
|
+
self,
|
|
26
|
+
metadata: dict[str, Any],
|
|
27
|
+
is_animated: bool = False,
|
|
28
|
+
done: bool = False,
|
|
29
|
+
animation_status: Literal["started", "stopped", "error"] | None = None,
|
|
30
|
+
) -> tuple[list[Segment], int]:
|
|
31
|
+
if not is_animated:
|
|
32
|
+
tag_segments, left_padding = super()._get_tag_segments(
|
|
33
|
+
metadata, is_animated, done, animation_status=animation_status
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
tag_style = metadata.get("tag_style")
|
|
37
|
+
|
|
38
|
+
if isinstance(tag_style, (str, Style)):
|
|
39
|
+
style = self.console.get_style(tag_style)
|
|
40
|
+
tag_segments = [
|
|
41
|
+
Segment(segment.text, style=style) for segment in tag_segments
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
return tag_segments, left_padding
|
|
45
|
+
|
|
46
|
+
emojis = [
|
|
47
|
+
"🥚",
|
|
48
|
+
"🐣",
|
|
49
|
+
"🐤",
|
|
50
|
+
"🐥",
|
|
51
|
+
"🐓",
|
|
52
|
+
"🐔",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
tag = emojis[self.animation_counter % len(emojis)]
|
|
56
|
+
|
|
57
|
+
if done:
|
|
58
|
+
tag = metadata.get("done_emoji", emojis[-1])
|
|
59
|
+
|
|
60
|
+
if animation_status == "error":
|
|
61
|
+
tag = "🟡"
|
|
62
|
+
|
|
63
|
+
left_padding = self.tag_width - 1
|
|
64
|
+
left_padding = max(0, left_padding)
|
|
65
|
+
|
|
66
|
+
return [Segment(tag)], left_padding
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FastAPIRichToolkit(RichToolkit):
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
style: BaseStyle | None = None,
|
|
73
|
+
theme: RichToolkitTheme | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
super().__init__(style=style, theme=theme)
|
|
76
|
+
self._version_check = self._get_version_check()
|
|
77
|
+
|
|
78
|
+
def __exit__(
|
|
79
|
+
self,
|
|
80
|
+
exc_type: type[BaseException] | None,
|
|
81
|
+
exc_value: BaseException | None,
|
|
82
|
+
traceback: TracebackType | None,
|
|
83
|
+
) -> bool | None:
|
|
84
|
+
self._print_update_message()
|
|
85
|
+
|
|
86
|
+
return super().__exit__(
|
|
87
|
+
exc_type,
|
|
88
|
+
exc_value,
|
|
89
|
+
traceback,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _get_version_check(self) -> BackgroundVersionCheck | None:
|
|
93
|
+
if os.environ.get(DISABLE_VERSION_CHECK_ENV) == "1":
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
version_check = BackgroundVersionCheck()
|
|
97
|
+
version_check.start()
|
|
98
|
+
|
|
99
|
+
return version_check
|
|
100
|
+
|
|
101
|
+
def _print_update_message(self) -> None:
|
|
102
|
+
if self._version_check is None:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if message := self._version_check.get_update_message():
|
|
106
|
+
self.print(Text.from_markup(message), tag="update", tag_style="tag.update")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_rich_toolkit(minimal: bool = False) -> RichToolkit:
|
|
110
|
+
style = MinimalStyle() if minimal else FastAPIStyle(tag_width=11)
|
|
111
|
+
|
|
112
|
+
theme = RichToolkitTheme(
|
|
113
|
+
style=style,
|
|
114
|
+
theme={
|
|
115
|
+
"tag.title": "white on #009485",
|
|
116
|
+
"tag": "white on #007166",
|
|
117
|
+
"tag.update": "black on yellow",
|
|
118
|
+
"placeholder": "grey62",
|
|
119
|
+
"text": "white",
|
|
120
|
+
"selected": "#007166",
|
|
121
|
+
"result": "grey85",
|
|
122
|
+
"progress": "on #007166",
|
|
123
|
+
"error": "red",
|
|
124
|
+
"cancelled": "indian_red italic",
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return FastAPIRichToolkit(theme=theme)
|