fastapi-cloud-cli 0.15.0__tar.gz → 0.16.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.15.0 → fastapi_cloud_cli-0.16.0}/PKG-INFO +1 -1
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/pyproject.toml +1 -1
- fastapi_cloud_cli-0.16.0/scripts/add_latest_release_date.py +40 -0
- fastapi_cloud_cli-0.16.0/src/fastapi_cloud_cli/__init__.py +1 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/deploy.py +32 -11
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/api.py +16 -12
- fastapi_cloud_cli-0.16.0/src/fastapi_cloud_cli/utils/progress_file.py +30 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_deploy.py +178 -3
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_deploy_utils.py +13 -12
- fastapi_cloud_cli-0.16.0/tests/test_progress_file.py +104 -0
- fastapi_cloud_cli-0.15.0/src/fastapi_cloud_cli/__init__.py +0 -1
- fastapi_cloud_cli-0.15.0/tests/assets/broken_package/mod/__init__.py +0 -1
- fastapi_cloud_cli-0.15.0/tests/assets/broken_package/mod/app.py +0 -10
- fastapi_cloud_cli-0.15.0/tests/assets/broken_package/utils.py +0 -2
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_api/api.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app/api.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app/app.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_api/app/api.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_app/app/api.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_app/app/app.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_main/app/api.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_main/app/app.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_main/app/main.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_main/api.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_main/app.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_main/main.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/default_files/non_default/nonstandard.py +0 -8
- fastapi_cloud_cli-0.15.0/tests/assets/package/__init__.py +0 -2
- fastapi_cloud_cli-0.15.0/tests/assets/package/core/__init__.py +0 -0
- fastapi_cloud_cli-0.15.0/tests/assets/package/core/utils.py +0 -2
- fastapi_cloud_cli-0.15.0/tests/assets/package/mod/__init__.py +0 -1
- fastapi_cloud_cli-0.15.0/tests/assets/package/mod/api.py +0 -24
- fastapi_cloud_cli-0.15.0/tests/assets/package/mod/app.py +0 -32
- fastapi_cloud_cli-0.15.0/tests/assets/package/mod/other.py +0 -16
- fastapi_cloud_cli-0.15.0/tests/assets/single_file_api.py +0 -24
- fastapi_cloud_cli-0.15.0/tests/assets/single_file_app.py +0 -32
- fastapi_cloud_cli-0.15.0/tests/assets/single_file_other.py +0 -16
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/README.md +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/cli.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/env.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/link.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/login.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/logs.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/setup_ci.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/whoami.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/logging.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/cli.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/conftest.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_api_client.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_archive.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_auth.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_link.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_login.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_setup_ci.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_unlink.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_cli_whoami.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_config.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_env_delete.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_env_list.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_env_set.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_logs.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/test_sentry.py +0 -0
- {fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/tests/utils.py +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Check release-notes.md and add today's date to the latest release header if missing."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import date
|
|
6
|
+
|
|
7
|
+
RELEASE_NOTES_FILE = "release-notes.md"
|
|
8
|
+
RELEASE_HEADER_PATTERN = re.compile(r"^## (\d+\.\d+\.\d+)\s*(\(.*\))?\s*$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
with open(RELEASE_NOTES_FILE) as f:
|
|
13
|
+
lines = f.readlines()
|
|
14
|
+
|
|
15
|
+
for i, line in enumerate(lines):
|
|
16
|
+
match = RELEASE_HEADER_PATTERN.match(line)
|
|
17
|
+
if not match:
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
version = match.group(1)
|
|
21
|
+
date_part = match.group(2)
|
|
22
|
+
|
|
23
|
+
if date_part:
|
|
24
|
+
print(f"Latest release {version} already has a date: {date_part}")
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
|
|
27
|
+
today = date.today().isoformat()
|
|
28
|
+
lines[i] = f"## {version} ({today})\n"
|
|
29
|
+
print(f"Added date: {version} ({today})")
|
|
30
|
+
|
|
31
|
+
with open(RELEASE_NOTES_FILE, "w") as f:
|
|
32
|
+
f.writelines(lines)
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
print("No release header found")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.16.0"
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/deploy.py
RENAMED
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
from itertools import cycle
|
|
8
8
|
from pathlib import Path, PurePosixPath
|
|
9
9
|
from textwrap import dedent
|
|
10
|
-
from typing import Annotated, Any
|
|
10
|
+
from typing import Annotated, Any, BinaryIO, cast
|
|
11
11
|
|
|
12
12
|
import fastar
|
|
13
13
|
import rignore
|
|
@@ -17,6 +17,7 @@ from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, Validatio
|
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
from rich_toolkit import RichToolkit
|
|
19
19
|
from rich_toolkit.menu import Option
|
|
20
|
+
from rich_toolkit.progress import Progress
|
|
20
21
|
|
|
21
22
|
from fastapi_cloud_cli.commands.login import login
|
|
22
23
|
from fastapi_cloud_cli.utils.api import (
|
|
@@ -29,6 +30,7 @@ from fastapi_cloud_cli.utils.api import (
|
|
|
29
30
|
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
|
|
30
31
|
from fastapi_cloud_cli.utils.auth import Identity
|
|
31
32
|
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
|
|
33
|
+
from fastapi_cloud_cli.utils.progress_file import ProgressFile
|
|
32
34
|
|
|
33
35
|
logger = logging.getLogger(__name__)
|
|
34
36
|
|
|
@@ -201,16 +203,32 @@ class RequestUploadResponse(BaseModel):
|
|
|
201
203
|
fields: dict[str, str]
|
|
202
204
|
|
|
203
205
|
|
|
204
|
-
def
|
|
206
|
+
def _format_size(size_in_bytes: int) -> str:
|
|
207
|
+
if size_in_bytes >= 1024 * 1024:
|
|
208
|
+
return f"{size_in_bytes / (1024 * 1024):.2f} MB"
|
|
209
|
+
elif size_in_bytes >= 1024:
|
|
210
|
+
return f"{size_in_bytes / 1024:.2f} KB"
|
|
211
|
+
else:
|
|
212
|
+
return f"{size_in_bytes} bytes"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _upload_deployment(
|
|
216
|
+
deployment_id: str, archive_path: Path, progress: Progress
|
|
217
|
+
) -> None:
|
|
218
|
+
archive_size = archive_path.stat().st_size
|
|
219
|
+
archive_size_str = _format_size(archive_size)
|
|
220
|
+
|
|
221
|
+
progress.log(f"Uploading deployment ({archive_size_str})...")
|
|
205
222
|
logger.debug(
|
|
206
223
|
"Starting deployment upload for deployment: %s",
|
|
207
224
|
deployment_id,
|
|
208
225
|
)
|
|
209
|
-
logger.debug(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
logger.debug("Archive path: %s, size: %s bytes", archive_path, archive_size)
|
|
227
|
+
|
|
228
|
+
def progress_callback(bytes_read: int) -> None:
|
|
229
|
+
progress.log(
|
|
230
|
+
f"Uploading deployment ({_format_size(bytes_read)} of {archive_size_str})..."
|
|
231
|
+
)
|
|
214
232
|
|
|
215
233
|
with APIClient() as fastapi_client, Client() as client:
|
|
216
234
|
# Get the upload URL
|
|
@@ -223,10 +241,13 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
|
|
|
223
241
|
|
|
224
242
|
logger.debug("Starting file upload to S3")
|
|
225
243
|
with open(archive_path, "rb") as archive_file:
|
|
244
|
+
archive_file_with_progress = ProgressFile(
|
|
245
|
+
archive_file, progress_callback=progress_callback
|
|
246
|
+
)
|
|
226
247
|
upload_response = client.post(
|
|
227
248
|
upload_data.url,
|
|
228
249
|
data=upload_data.fields,
|
|
229
|
-
files={"file":
|
|
250
|
+
files={"file": cast(BinaryIO, archive_file_with_progress)},
|
|
230
251
|
)
|
|
231
252
|
|
|
232
253
|
upload_response.raise_for_status()
|
|
@@ -304,6 +325,7 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
304
325
|
"Select the team you want to deploy to:",
|
|
305
326
|
tag="team",
|
|
306
327
|
options=[Option({"name": team.name, "value": team}) for team in teams],
|
|
328
|
+
allow_filtering=True,
|
|
307
329
|
)
|
|
308
330
|
|
|
309
331
|
toolkit.print_line()
|
|
@@ -335,6 +357,7 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
|
|
|
335
357
|
selected_app = toolkit.ask(
|
|
336
358
|
"Select the app you want to deploy to:",
|
|
337
359
|
options=[Option({"name": app.slug, "value": app}) for app in apps],
|
|
360
|
+
allow_filtering=True,
|
|
338
361
|
)
|
|
339
362
|
|
|
340
363
|
app_name = (
|
|
@@ -767,9 +790,7 @@ def deploy(
|
|
|
767
790
|
f"Deployment created successfully! Deployment slug: {deployment.slug}"
|
|
768
791
|
)
|
|
769
792
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
_upload_deployment(deployment.id, archive_path)
|
|
793
|
+
_upload_deployment(deployment.id, archive_path, progress=progress)
|
|
773
794
|
|
|
774
795
|
progress.log("Deployment uploaded successfully!")
|
|
775
796
|
except KeyboardInterrupt:
|
|
@@ -141,6 +141,7 @@ def attempts(
|
|
|
141
141
|
|
|
142
142
|
class DeploymentStatus(str, Enum):
|
|
143
143
|
waiting_upload = "waiting_upload"
|
|
144
|
+
upload_cancelled = "upload_cancelled"
|
|
144
145
|
ready_for_build = "ready_for_build"
|
|
145
146
|
building = "building"
|
|
146
147
|
extracting = "extracting"
|
|
@@ -153,24 +154,27 @@ class DeploymentStatus(str, Enum):
|
|
|
153
154
|
verifying_failed = "verifying_failed"
|
|
154
155
|
verifying_skipped = "verifying_skipped"
|
|
155
156
|
success = "success"
|
|
157
|
+
expired = "expired"
|
|
156
158
|
failed = "failed"
|
|
157
159
|
|
|
158
160
|
@classmethod
|
|
159
161
|
def to_human_readable(cls, status: "DeploymentStatus") -> str:
|
|
160
162
|
return {
|
|
161
|
-
cls.waiting_upload: "
|
|
162
|
-
cls.
|
|
163
|
+
cls.waiting_upload: "Awaiting Upload",
|
|
164
|
+
cls.upload_cancelled: "Upload Cancelled",
|
|
165
|
+
cls.ready_for_build: "Build Queued",
|
|
163
166
|
cls.building: "Building",
|
|
164
|
-
cls.extracting: "Extracting",
|
|
165
|
-
cls.extracting_failed: "
|
|
166
|
-
cls.building_image: "Building
|
|
167
|
-
cls.building_image_failed: "Build
|
|
168
|
-
cls.deploying: "Deploying",
|
|
169
|
-
cls.deploying_failed: "
|
|
170
|
-
cls.verifying: "Verifying",
|
|
171
|
-
cls.verifying_failed: "
|
|
172
|
-
cls.verifying_skipped: "Verification
|
|
173
|
-
cls.success: "
|
|
167
|
+
cls.extracting: "Extracting Upload",
|
|
168
|
+
cls.extracting_failed: "Extraction Failed",
|
|
169
|
+
cls.building_image: "Building Image",
|
|
170
|
+
cls.building_image_failed: "Build Failed",
|
|
171
|
+
cls.deploying: "Deploying Image",
|
|
172
|
+
cls.deploying_failed: "Deployment Failed",
|
|
173
|
+
cls.verifying: "Verifying Readiness",
|
|
174
|
+
cls.verifying_failed: "Verification Failed",
|
|
175
|
+
cls.verifying_skipped: "Verification Skipped",
|
|
176
|
+
cls.success: "Ready",
|
|
177
|
+
cls.expired: "Expired",
|
|
174
178
|
cls.failed: "Failed",
|
|
175
179
|
}[status]
|
|
176
180
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, BinaryIO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProgressFile:
|
|
7
|
+
"""Wrap a binary file object and report upload progress as it is read."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
file: BinaryIO,
|
|
12
|
+
progress_callback: Callable[[int], None],
|
|
13
|
+
update_interval: float = 0.5,
|
|
14
|
+
) -> None:
|
|
15
|
+
self._file = file
|
|
16
|
+
self._progress_callback = progress_callback
|
|
17
|
+
self._update_interval = update_interval
|
|
18
|
+
self._last_update_time = 0.0
|
|
19
|
+
|
|
20
|
+
def read(self, n: int = -1) -> bytes:
|
|
21
|
+
data = self._file.read(n)
|
|
22
|
+
now_ = datetime.now().timestamp()
|
|
23
|
+
is_eof = (len(data) == 0) or (n > 0 and len(data) < n)
|
|
24
|
+
if (now_ - self._last_update_time >= self._update_interval) or is_eof:
|
|
25
|
+
self._progress_callback(self._file.tell())
|
|
26
|
+
self._last_update_time = now_
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str) -> Any:
|
|
30
|
+
return getattr(self._file, name)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import random
|
|
2
|
+
import re
|
|
2
3
|
import string
|
|
3
4
|
from datetime import timedelta
|
|
4
5
|
from pathlib import Path
|
|
@@ -10,6 +11,7 @@ import pytest
|
|
|
10
11
|
import respx
|
|
11
12
|
from click.testing import Result
|
|
12
13
|
from httpx import Response
|
|
14
|
+
from rich_toolkit.progress import Progress
|
|
13
15
|
from time_machine import TimeMachineFixture
|
|
14
16
|
from typer.testing import CliRunner
|
|
15
17
|
|
|
@@ -24,8 +26,8 @@ runner = CliRunner()
|
|
|
24
26
|
assets_path = Path(__file__).parent / "assets"
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def _get_random_team() -> dict[str, str]:
|
|
28
|
-
name = "".join(random.choices(string.ascii_lowercase, k=10))
|
|
29
|
+
def _get_random_team(name: str | None = None) -> dict[str, str]:
|
|
30
|
+
name = name or "".join(random.choices(string.ascii_lowercase, k=10))
|
|
29
31
|
slug = "".join(random.choices(string.ascii_lowercase, k=10))
|
|
30
32
|
id = "".join(random.choices(string.digits, k=10))
|
|
31
33
|
|
|
@@ -323,6 +325,42 @@ def test_shows_teams(
|
|
|
323
325
|
assert team_2["name"] in result.output
|
|
324
326
|
|
|
325
327
|
|
|
328
|
+
@pytest.mark.respx
|
|
329
|
+
def test_filter_teams(
|
|
330
|
+
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
331
|
+
) -> None:
|
|
332
|
+
steps = [*"al", Keys.ENTER, Keys.CTRL_C]
|
|
333
|
+
|
|
334
|
+
team_1 = _get_random_team(name="Alpha Team")
|
|
335
|
+
team_2 = _get_random_team(name="Beta Team")
|
|
336
|
+
|
|
337
|
+
respx_mock.get("/teams/").mock(
|
|
338
|
+
return_value=Response(
|
|
339
|
+
200,
|
|
340
|
+
json={"data": [team_1, team_2]},
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
with (
|
|
345
|
+
changing_dir(tmp_path),
|
|
346
|
+
patch("rich_toolkit.container.getchar") as mock_getchar,
|
|
347
|
+
):
|
|
348
|
+
mock_getchar.side_effect = steps
|
|
349
|
+
|
|
350
|
+
result = runner.invoke(app, ["deploy"])
|
|
351
|
+
|
|
352
|
+
assert result.exit_code == 1
|
|
353
|
+
|
|
354
|
+
assert "Filter: al" in result.output
|
|
355
|
+
|
|
356
|
+
# Truncate part of the output before "Filter: al"
|
|
357
|
+
filer_pos = result.output.rfind("Filter: al")
|
|
358
|
+
last_output = result.output[filer_pos:]
|
|
359
|
+
|
|
360
|
+
assert team_1["name"] in last_output
|
|
361
|
+
assert team_2["name"] not in last_output
|
|
362
|
+
|
|
363
|
+
|
|
326
364
|
@pytest.mark.respx
|
|
327
365
|
def test_asks_for_app_name_after_team(
|
|
328
366
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
@@ -349,6 +387,48 @@ def test_asks_for_app_name_after_team(
|
|
|
349
387
|
assert "What's your app name?" in result.output
|
|
350
388
|
|
|
351
389
|
|
|
390
|
+
@pytest.mark.respx
|
|
391
|
+
def test_filter_apps(
|
|
392
|
+
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
393
|
+
) -> None:
|
|
394
|
+
steps = [Keys.ENTER, Keys.RIGHT_ARROW, Keys.ENTER, *"an", Keys.ENTER, Keys.CTRL_C]
|
|
395
|
+
|
|
396
|
+
team = _get_random_team()
|
|
397
|
+
|
|
398
|
+
respx_mock.get("/teams/").mock(
|
|
399
|
+
return_value=Response(
|
|
400
|
+
200,
|
|
401
|
+
json={"data": [team]},
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
app_1 = _get_random_app(team_id=team["id"], slug="My App")
|
|
406
|
+
app_2 = _get_random_app(team_id=team["id"], slug="Another App")
|
|
407
|
+
|
|
408
|
+
respx_mock.get("/apps/", params={"team_id": team["id"]}).mock(
|
|
409
|
+
return_value=Response(200, json={"data": [app_1, app_2]})
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
with (
|
|
413
|
+
changing_dir(tmp_path),
|
|
414
|
+
patch("rich_toolkit.container.getchar") as mock_getchar,
|
|
415
|
+
):
|
|
416
|
+
mock_getchar.side_effect = steps
|
|
417
|
+
|
|
418
|
+
result = runner.invoke(app, ["deploy"])
|
|
419
|
+
|
|
420
|
+
assert result.exit_code == 1
|
|
421
|
+
|
|
422
|
+
assert "Filter: an" in result.output
|
|
423
|
+
|
|
424
|
+
# Truncate part of the output before "Filter: an"
|
|
425
|
+
filer_pos = result.output.rfind("Filter: an")
|
|
426
|
+
last_output = result.output[filer_pos:]
|
|
427
|
+
|
|
428
|
+
assert app_1["slug"] not in last_output
|
|
429
|
+
assert app_2["slug"] in last_output
|
|
430
|
+
|
|
431
|
+
|
|
352
432
|
@pytest.mark.respx
|
|
353
433
|
def test_creates_app_on_backend(
|
|
354
434
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
@@ -1578,6 +1658,101 @@ def test_deploy_with_token_fails(
|
|
|
1578
1658
|
)
|
|
1579
1659
|
|
|
1580
1660
|
|
|
1661
|
+
@pytest.mark.parametrize(
|
|
1662
|
+
("size", "expected_msgs"),
|
|
1663
|
+
[
|
|
1664
|
+
(
|
|
1665
|
+
100,
|
|
1666
|
+
[
|
|
1667
|
+
r"\(\d+ bytes\)", # e.g. "(123 bytes)"
|
|
1668
|
+
r"\(\d+ bytes of \d+ bytes\)", # e.g. "(123 bytes of 456 bytes)"
|
|
1669
|
+
],
|
|
1670
|
+
),
|
|
1671
|
+
(
|
|
1672
|
+
10 * 1024,
|
|
1673
|
+
[
|
|
1674
|
+
r"\(\d+\.\d+ KB\)", # e.g. "(1.23 KB)"
|
|
1675
|
+
r"\(\d+\.\d+ KB of \d+\.\d+ KB\)", # e.g. "(1.23 KB of 4.56 KB)"
|
|
1676
|
+
],
|
|
1677
|
+
),
|
|
1678
|
+
(
|
|
1679
|
+
10 * 1024 * 1024,
|
|
1680
|
+
[
|
|
1681
|
+
r"\(\d+\.\d+ MB\)", # e.g. "(1.23 MB)"
|
|
1682
|
+
r"\(\d+\.\d+ KB of \d+\.\d+ MB\)", # e.g. "(1.23 KB of 4.56 MB)"
|
|
1683
|
+
r"\(\d+\.\d+ MB of \d+\.\d+ MB\)", # e.g. "(1.23 MB of 4.56 MB)"
|
|
1684
|
+
],
|
|
1685
|
+
),
|
|
1686
|
+
],
|
|
1687
|
+
)
|
|
1688
|
+
@pytest.mark.respx
|
|
1689
|
+
def test_upload_deployment_progress(
|
|
1690
|
+
logged_in_cli: None,
|
|
1691
|
+
tmp_path: Path,
|
|
1692
|
+
respx_mock: respx.MockRouter,
|
|
1693
|
+
size: int,
|
|
1694
|
+
expected_msgs: list[str],
|
|
1695
|
+
) -> None:
|
|
1696
|
+
app_data = _get_random_app()
|
|
1697
|
+
team_data = _get_random_team()
|
|
1698
|
+
app_id = app_data["id"]
|
|
1699
|
+
team_id = team_data["id"]
|
|
1700
|
+
deployment_data = _get_random_deployment(app_id=app_id)
|
|
1701
|
+
deployment_id = deployment_data["id"]
|
|
1702
|
+
|
|
1703
|
+
config_path = tmp_path / ".fastapicloud" / "cloud.json"
|
|
1704
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1705
|
+
config_path.write_text(f'{{"app_id": "{app_id}", "team_id": "{team_id}"}}')
|
|
1706
|
+
|
|
1707
|
+
(tmp_path / "file.bin").write_bytes(random.randbytes(size))
|
|
1708
|
+
|
|
1709
|
+
respx_mock.get(f"/apps/{app_id}").mock(return_value=Response(200, json=app_data))
|
|
1710
|
+
respx_mock.post(f"/apps/{app_id}/deployments/").mock(
|
|
1711
|
+
return_value=Response(201, json=deployment_data)
|
|
1712
|
+
)
|
|
1713
|
+
respx_mock.post(f"/deployments/{deployment_id}/upload").mock(
|
|
1714
|
+
return_value=Response(
|
|
1715
|
+
200,
|
|
1716
|
+
json={"url": "http://test.com", "fields": {"key": "value"}},
|
|
1717
|
+
)
|
|
1718
|
+
)
|
|
1719
|
+
respx_mock.post("http://test.com", data={"key": "value"}).mock(
|
|
1720
|
+
return_value=Response(200)
|
|
1721
|
+
)
|
|
1722
|
+
respx_mock.post(f"/deployments/{deployment_id}/upload-complete").mock(
|
|
1723
|
+
return_value=Response(200)
|
|
1724
|
+
)
|
|
1725
|
+
respx_mock.get(f"/deployments/{deployment_id}/build-logs").mock(
|
|
1726
|
+
return_value=Response(
|
|
1727
|
+
200,
|
|
1728
|
+
content=build_logs_response(
|
|
1729
|
+
{"type": "message", "message": "Building...", "id": "1"},
|
|
1730
|
+
{"type": "complete"},
|
|
1731
|
+
),
|
|
1732
|
+
)
|
|
1733
|
+
)
|
|
1734
|
+
respx_mock.get(f"/apps/{app_id}/deployments/{deployment_id}").mock(
|
|
1735
|
+
return_value=Response(200, json={**deployment_data, "status": "success"})
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
with (
|
|
1739
|
+
changing_dir(tmp_path),
|
|
1740
|
+
patch.object(Progress, "log") as mock_progress,
|
|
1741
|
+
):
|
|
1742
|
+
result = runner.invoke(app, ["deploy"])
|
|
1743
|
+
assert result.exit_code == 0
|
|
1744
|
+
|
|
1745
|
+
call_args = [
|
|
1746
|
+
c.args[0] for c in mock_progress.call_args_list if isinstance(c.args[0], str)
|
|
1747
|
+
]
|
|
1748
|
+
|
|
1749
|
+
for expected_msg in expected_msgs:
|
|
1750
|
+
pattern = re.compile(f"Uploading deployment {expected_msg}\\.\\.\\.")
|
|
1751
|
+
assert any(pattern.match(arg) for arg in call_args), (
|
|
1752
|
+
f"Expected message '{pattern.pattern}' not found in {call_args}"
|
|
1753
|
+
)
|
|
1754
|
+
|
|
1755
|
+
|
|
1581
1756
|
@pytest.mark.respx
|
|
1582
1757
|
def test_deploy_with_app_id_arg(
|
|
1583
1758
|
logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter
|
|
@@ -1838,7 +2013,7 @@ def test_verification_failure_after_build_complete(
|
|
|
1838
2013
|
|
|
1839
2014
|
assert result.exit_code == 1
|
|
1840
2015
|
assert "Deployment failed" in result.output
|
|
1841
|
-
assert "
|
|
2016
|
+
assert "Verification Failed" in result.output
|
|
1842
2017
|
assert deployment_data["dashboard_url"] in result.output
|
|
1843
2018
|
|
|
1844
2019
|
|
|
@@ -58,19 +58,20 @@ def test_includes_paths(path: Path) -> None:
|
|
|
58
58
|
@pytest.mark.parametrize(
|
|
59
59
|
"status,expected",
|
|
60
60
|
[
|
|
61
|
-
(DeploymentStatus.waiting_upload, "
|
|
62
|
-
(DeploymentStatus.ready_for_build, "
|
|
61
|
+
(DeploymentStatus.waiting_upload, "Awaiting Upload"),
|
|
62
|
+
(DeploymentStatus.ready_for_build, "Build Queued"),
|
|
63
63
|
(DeploymentStatus.building, "Building"),
|
|
64
|
-
(DeploymentStatus.extracting, "Extracting"),
|
|
65
|
-
(DeploymentStatus.extracting_failed, "
|
|
66
|
-
(DeploymentStatus.building_image, "Building
|
|
67
|
-
(DeploymentStatus.building_image_failed, "Build
|
|
68
|
-
(DeploymentStatus.deploying, "Deploying"),
|
|
69
|
-
(DeploymentStatus.deploying_failed, "
|
|
70
|
-
(DeploymentStatus.verifying, "Verifying"),
|
|
71
|
-
(DeploymentStatus.verifying_failed, "
|
|
72
|
-
(DeploymentStatus.verifying_skipped, "Verification
|
|
73
|
-
(DeploymentStatus.success, "
|
|
64
|
+
(DeploymentStatus.extracting, "Extracting Upload"),
|
|
65
|
+
(DeploymentStatus.extracting_failed, "Extraction Failed"),
|
|
66
|
+
(DeploymentStatus.building_image, "Building Image"),
|
|
67
|
+
(DeploymentStatus.building_image_failed, "Build Failed"),
|
|
68
|
+
(DeploymentStatus.deploying, "Deploying Image"),
|
|
69
|
+
(DeploymentStatus.deploying_failed, "Deployment Failed"),
|
|
70
|
+
(DeploymentStatus.verifying, "Verifying Readiness"),
|
|
71
|
+
(DeploymentStatus.verifying_failed, "Verification Failed"),
|
|
72
|
+
(DeploymentStatus.verifying_skipped, "Verification Skipped"),
|
|
73
|
+
(DeploymentStatus.success, "Ready"),
|
|
74
|
+
(DeploymentStatus.expired, "Expired"),
|
|
74
75
|
(DeploymentStatus.failed, "Failed"),
|
|
75
76
|
],
|
|
76
77
|
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from unittest.mock import Mock, call
|
|
4
|
+
|
|
5
|
+
import time_machine
|
|
6
|
+
|
|
7
|
+
from fastapi_cloud_cli.utils.progress_file import ProgressFile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _make_file(
|
|
11
|
+
content: bytes = b"hello world", name: str = "test.tar.gz"
|
|
12
|
+
) -> io.BytesIO:
|
|
13
|
+
f = io.BytesIO(content)
|
|
14
|
+
f.name = name
|
|
15
|
+
return f
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_read_with_size() -> None:
|
|
19
|
+
file = _make_file(b"abcdef")
|
|
20
|
+
pf = ProgressFile(file, progress_callback=lambda _: None)
|
|
21
|
+
|
|
22
|
+
assert pf.read(3) == b"abc"
|
|
23
|
+
assert pf.read(3) == b"def"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_callback_not_called_within_interval() -> None:
|
|
27
|
+
file = _make_file(b"abcdef")
|
|
28
|
+
mock_callback = Mock()
|
|
29
|
+
pf = ProgressFile(file, progress_callback=mock_callback)
|
|
30
|
+
|
|
31
|
+
pf.read(3) # Should trigger callback
|
|
32
|
+
pf.read(3) # Should NOT trigger
|
|
33
|
+
|
|
34
|
+
mock_callback.assert_called_once_with(3)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_callback_called_after_interval_elapses() -> None:
|
|
38
|
+
file = _make_file(b"abcdef")
|
|
39
|
+
mock_callback = Mock()
|
|
40
|
+
|
|
41
|
+
with time_machine.travel(
|
|
42
|
+
datetime(2026, 1, 1, tzinfo=timezone.utc), tick=False
|
|
43
|
+
) as traveller:
|
|
44
|
+
pf = ProgressFile(file, progress_callback=mock_callback)
|
|
45
|
+
|
|
46
|
+
pf.read(3)
|
|
47
|
+
traveller.shift(0.6)
|
|
48
|
+
pf.read(3)
|
|
49
|
+
|
|
50
|
+
mock_callback.assert_has_calls([call(3), call(6)])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_callback_tracks_cumulative_bytes() -> None:
|
|
54
|
+
file = _make_file(b"a" * 100)
|
|
55
|
+
mock_callback = Mock()
|
|
56
|
+
|
|
57
|
+
with time_machine.travel(
|
|
58
|
+
datetime(2026, 1, 1, tzinfo=timezone.utc), tick=False
|
|
59
|
+
) as traveller:
|
|
60
|
+
pf = ProgressFile(file, progress_callback=mock_callback)
|
|
61
|
+
|
|
62
|
+
pf.read(10) # Should trigger callback with 10 bytes read
|
|
63
|
+
traveller.shift(0.1)
|
|
64
|
+
pf.read(10)
|
|
65
|
+
traveller.shift(0.5)
|
|
66
|
+
pf.read(10) # Should trigger callback with 10 + 10 + 10 = 30 bytes read
|
|
67
|
+
traveller.shift(0.6)
|
|
68
|
+
pf.read(10) # Should trigger callback with 30 + 10 = 40 bytes read
|
|
69
|
+
|
|
70
|
+
mock_callback.assert_has_calls([call(10), call(30), call(40)])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_callback_called_on_eof() -> None:
|
|
74
|
+
file = _make_file(b"abcd")
|
|
75
|
+
mock_callback = Mock()
|
|
76
|
+
|
|
77
|
+
pf = ProgressFile(file, progress_callback=mock_callback)
|
|
78
|
+
pf.read(3)
|
|
79
|
+
pf.read(3)
|
|
80
|
+
mock_callback.assert_has_calls([call(3), call(4)])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_name_property() -> None:
|
|
84
|
+
file = _make_file(name="test.tar.gz")
|
|
85
|
+
pf = ProgressFile(file, progress_callback=lambda _: None)
|
|
86
|
+
|
|
87
|
+
assert pf.name == "test.tar.gz"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_callback_uses_current_file_position_after_seek() -> None:
|
|
91
|
+
file = _make_file(b"abcde")
|
|
92
|
+
mock_callback = Mock()
|
|
93
|
+
|
|
94
|
+
pf = ProgressFile(file, progress_callback=mock_callback)
|
|
95
|
+
|
|
96
|
+
pf.read(3)
|
|
97
|
+
|
|
98
|
+
# Imitate retrying
|
|
99
|
+
pf.seek(0)
|
|
100
|
+
pf.read(3)
|
|
101
|
+
|
|
102
|
+
pf.read(3)
|
|
103
|
+
|
|
104
|
+
mock_callback.assert_has_calls([call(3), call(5)])
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.15.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .app import app as app
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
fastapi_cloud_cli-0.15.0/tests/assets/default_files/default_app_dir_non_default/app/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .app import app as app
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
first_other = FastAPI()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@first_other.get("/")
|
|
7
|
-
def first_other_root():
|
|
8
|
-
return {"message": "package first_other"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
second_other = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@second_other.get("/")
|
|
15
|
-
def second_other_root():
|
|
16
|
-
return {"message": "package second_other"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
api = FastAPI()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@api.get("/")
|
|
23
|
-
def api_root():
|
|
24
|
-
return {"message": "package api"}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
first_other = FastAPI()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@first_other.get("/")
|
|
7
|
-
def first_other_root():
|
|
8
|
-
return {"message": "package first_other"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
second_other = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@second_other.get("/")
|
|
15
|
-
def second_other_root():
|
|
16
|
-
return {"message": "package second_other"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
api = FastAPI()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@api.get("/")
|
|
23
|
-
def api_root():
|
|
24
|
-
return {"message": "package api"}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
app = FastAPI()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@app.get("/")
|
|
31
|
-
def app_root():
|
|
32
|
-
return {"message": "package app"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
first_other = FastAPI()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@first_other.get("/")
|
|
7
|
-
def first_other_root():
|
|
8
|
-
return {"message": "package first_other"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
second_other = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@second_other.get("/")
|
|
15
|
-
def second_other_root():
|
|
16
|
-
return {"message": "package second_other"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
first_other = FastAPI()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@first_other.get("/")
|
|
7
|
-
def first_other_root():
|
|
8
|
-
return {"message": "single file first_other"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
second_other = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@second_other.get("/")
|
|
15
|
-
def second_other_root():
|
|
16
|
-
return {"message": "single file second_other"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
api = FastAPI()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@api.get("/")
|
|
23
|
-
def api_root():
|
|
24
|
-
return {"message": "single file api"}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
first_other = FastAPI()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@first_other.get("/")
|
|
7
|
-
def first_other_root():
|
|
8
|
-
return {"message": "single file first_other"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
second_other = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@second_other.get("/")
|
|
15
|
-
def second_other_root():
|
|
16
|
-
return {"message": "single file second_other"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
api = FastAPI()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@api.get("/")
|
|
23
|
-
def api_root():
|
|
24
|
-
return {"message": "single file api"}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
app = FastAPI()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@app.get("/")
|
|
31
|
-
def app_root():
|
|
32
|
-
return {"message": "single file app"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
|
|
3
|
-
first_other = FastAPI()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@first_other.get("/")
|
|
7
|
-
def first_other_root():
|
|
8
|
-
return {"message": "single file first_other"}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
second_other = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@second_other.get("/")
|
|
15
|
-
def second_other_root():
|
|
16
|
-
return {"message": "single file second_other"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/link.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/login.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/logout.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/logs.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/setup_ci.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/unlink.py
RENAMED
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/commands/whoami.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_cloud_cli-0.15.0 → fastapi_cloud_cli-0.16.0}/src/fastapi_cloud_cli/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|