fastapi-cloud-cli 0.23.0__tar.gz → 0.25.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.23.0 → fastapi_cloud_cli-0.25.0}/PKG-INFO +3 -2
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/pyproject.toml +8 -3
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/__init__.py +1 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/_app.py +35 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/api/__init__.py +15 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/api/_errors.py +136 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/api/_models.py +140 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/api/_retry.py +91 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/api/client.py +308 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/cli.py +78 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/_auth.py +41 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/_flow.py +1 -1
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/apps/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/apps/_app.py +6 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/apps/create.py +191 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/apps/get.py +42 -46
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/apps/link.py +6 -2
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/apps/list.py +52 -98
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/apps/unlink.py +4 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/apps/update.py +117 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/auth/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/auth/_app.py +6 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/auth/wait.py +3 -1
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/ci/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/ci/_app.py +6 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/ci/print_workflow.py +2 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/cloud.py +1 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/command.py +4 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/configure.py +1 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/upload.py +1 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/wait.py +3 -3
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deployments.py +91 -110
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/_app.py +6 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/_setup.py +211 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/_shared.py +40 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/add.py +186 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/get.py +120 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/list.py +77 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/remove.py +174 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/rendering.py +364 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/domains/restart.py +150 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/env/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/env/_app.py +6 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/env/_shared.py +1 -1
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/env/delete.py +188 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/env/get.py +133 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/env/list.py +25 -31
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/env/set.py +49 -54
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/__init__.py +13 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/providers/__init__.py +5 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/providers/_app.py +6 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/providers/list.py +139 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/__init__.py +5 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/_app.py +6 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/connect.py +150 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/disconnect.py +244 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/get.py +226 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/list.py +118 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/integrations/resources/providers.py +10 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/login.py +6 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/logout.py +2 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/logs.py +34 -36
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/setup_ci.py +209 -220
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/teams/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/teams/_app.py +6 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/teams/get.py +24 -28
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/teams/__init__.py → fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/teams/list.py +22 -41
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/tokens/__init__.py +3 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/tokens/_app.py +6 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/tokens/create.py +44 -48
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/tokens/delete.py +97 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/tokens/list.py +27 -32
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/commands/whoami.py +66 -0
- fastapi_cloud_cli-0.25.0/src/fastapi_cloud_cli/utils/teams.py +83 -0
- fastapi_cloud_cli-0.25.0/tests/domains/__init__.py +0 -0
- fastapi_cloud_cli-0.25.0/tests/domains/test_cli.py +1381 -0
- fastapi_cloud_cli-0.25.0/tests/domains/test_setup.py +190 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/__init__.py +0 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/providers/__init__.py +0 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/providers/test_list.py +299 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/resources/__init__.py +0 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/resources/test_connect.py +226 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/resources/test_disconnect.py +393 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/resources/test_get.py +313 -0
- fastapi_cloud_cli-0.25.0/tests/integrations/resources/test_list.py +262 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_api_client.py +31 -5
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_apps.py +70 -31
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_deploy.py +5 -5
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_deployments.py +36 -30
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_link.py +1 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_login.py +5 -5
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_teams.py +33 -19
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_tokens.py +18 -18
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_whoami.py +3 -3
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_deploy_utils.py +13 -1
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_env_delete.py +40 -23
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_env_get.py +32 -14
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_env_list.py +39 -26
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_env_set.py +96 -16
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_logs.py +2 -2
- fastapi_cloud_cli-0.25.0/tests/utils.py +115 -0
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/__init__.py +0 -1
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/cli.py +0 -88
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/apps/__init__.py +0 -23
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/apps/create.py +0 -192
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/apps/update.py +0 -122
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/auth/__init__.py +0 -14
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/ci/__init__.py +0 -15
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/env/__init__.py +0 -17
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/env/delete.py +0 -194
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/env/get.py +0 -139
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/tokens/__init__.py +0 -15
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/tokens/delete.py +0 -102
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/commands/whoami.py +0 -68
- fastapi_cloud_cli-0.23.0/src/fastapi_cloud_cli/utils/api.py +0 -518
- fastapi_cloud_cli-0.23.0/tests/utils.py +0 -62
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/LICENSE +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/README.md +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/scripts/add_latest_release_date.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/scripts/format.sh +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/scripts/lint.sh +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/scripts/prepare_release.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/scripts/test-cov-html.sh +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/scripts/test.sh +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/__main__.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/__init__.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/commands/deploy/archive.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/config.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/logging.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/py.typed +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/apps.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/auth.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/cli.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/dates.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/errors.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/execution.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/progress_file.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/sentry.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/src/fastapi_cloud_cli/utils/version_check.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/__init__.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/conftest.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_archive.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_auth.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_ci.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_logout.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_setup_ci.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_cli_unlink.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_config.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_dates.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_prepare_release.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_progress_file.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_sentry.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_utils_apps.py +0 -0
- {fastapi_cloud_cli-0.23.0 → fastapi_cloud_cli-0.25.0}/tests/test_version_check.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.25.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
|
|
@@ -29,7 +29,7 @@ Project-URL: Repository, https://github.com/fastapilabs/fastapi-cloud-cli
|
|
|
29
29
|
Project-URL: Issues, https://github.com/fastapilabs/fastapi-cloud-cli/issues
|
|
30
30
|
Project-URL: Changelog, https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-notes.md
|
|
31
31
|
Requires-Python: >=3.10
|
|
32
|
-
Requires-Dist: typer>=0.
|
|
32
|
+
Requires-Dist: typer>=0.26.1
|
|
33
33
|
Requires-Dist: uvicorn[standard]>=0.17.6
|
|
34
34
|
Requires-Dist: rignore>=0.5.1
|
|
35
35
|
Requires-Dist: httpx>=0.27.0
|
|
@@ -40,6 +40,7 @@ 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
42
|
Requires-Dist: detect-installer>=0.1.0
|
|
43
|
+
Requires-Dist: agent-detector>=1.1.0
|
|
43
44
|
Provides-Extra: standard
|
|
44
45
|
Requires-Dist: uvicorn[standard]>=0.15.0; extra == "standard"
|
|
45
46
|
Description-Content-Type: text/markdown
|
|
@@ -29,7 +29,7 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.13",
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
|
-
"typer >= 0.
|
|
32
|
+
"typer >= 0.26.1",
|
|
33
33
|
"uvicorn[standard] >= 0.17.6",
|
|
34
34
|
"rignore >= 0.5.1",
|
|
35
35
|
"httpx >= 0.27.0",
|
|
@@ -40,8 +40,9 @@ dependencies = [
|
|
|
40
40
|
"sentry-sdk >= 2.20.0",
|
|
41
41
|
"fastar >= 0.10.0",
|
|
42
42
|
"detect-installer>=0.1.0",
|
|
43
|
+
"agent-detector>=1.1.0",
|
|
43
44
|
]
|
|
44
|
-
version = "0.
|
|
45
|
+
version = "0.25.0"
|
|
45
46
|
|
|
46
47
|
[project.license]
|
|
47
48
|
text = "MIT"
|
|
@@ -63,12 +64,13 @@ dev = [
|
|
|
63
64
|
"prek>=0.2.24,<1.0.0",
|
|
64
65
|
"pytest>=7.0.0,<10.0.0",
|
|
65
66
|
"coverage[toml]>=7.2,<8.0",
|
|
66
|
-
"mypy==2.1
|
|
67
|
+
"mypy==2.3.1",
|
|
67
68
|
"ruff>=0.15.16",
|
|
68
69
|
"respx==0.23.1",
|
|
69
70
|
"time-machine>=2.19.0",
|
|
70
71
|
"ty>=0.0.25",
|
|
71
72
|
"zizmor>=1.24.1",
|
|
73
|
+
"inline-snapshot>=0.35.4",
|
|
72
74
|
]
|
|
73
75
|
github-actions = [
|
|
74
76
|
"smokeshow>=0.5.0",
|
|
@@ -101,6 +103,9 @@ addopts = [
|
|
|
101
103
|
xfail_strict = true
|
|
102
104
|
junit_family = "xunit2"
|
|
103
105
|
|
|
106
|
+
[tool.inline-snapshot]
|
|
107
|
+
format-command = "ruff format --stdin-filename {filename}"
|
|
108
|
+
|
|
104
109
|
[tool.coverage.run]
|
|
105
110
|
parallel = true
|
|
106
111
|
data_file = "coverage/.coverage"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.25.0"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
|
|
6
|
+
from fastapi_cloud_cli import __version__
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(rich_markup_mode="rich")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def version_callback(value: bool) -> None:
|
|
12
|
+
if value:
|
|
13
|
+
print(f"FastAPI Cloud CLI version: [green]{__version__}[/green]")
|
|
14
|
+
raise typer.Exit()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
cloud_app = typer.Typer(
|
|
18
|
+
rich_markup_mode="rich",
|
|
19
|
+
help="Manage [bold]FastAPI[/bold] Cloud deployments.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cloud_app.callback()
|
|
25
|
+
def cloud_main(
|
|
26
|
+
version: Annotated[
|
|
27
|
+
bool,
|
|
28
|
+
typer.Option(
|
|
29
|
+
"--version",
|
|
30
|
+
callback=version_callback,
|
|
31
|
+
is_eager=True,
|
|
32
|
+
help="Show the version and exit.",
|
|
33
|
+
),
|
|
34
|
+
] = False,
|
|
35
|
+
) -> None: ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ._errors import StreamLogError as StreamLogError
|
|
2
|
+
from ._errors import TooManyRetriesError as TooManyRetriesError
|
|
3
|
+
from ._errors import _get_response_error_message as _get_response_error_message
|
|
4
|
+
from ._errors import get_http_error_code as get_http_error_code
|
|
5
|
+
from ._errors import get_http_error_hint as get_http_error_hint
|
|
6
|
+
from ._errors import handle_http_error as handle_http_error
|
|
7
|
+
from ._models import SUCCESSFUL_STATUSES as SUCCESSFUL_STATUSES
|
|
8
|
+
from ._models import AppLogEntry as AppLogEntry
|
|
9
|
+
from ._models import BuildLogLineMessage as BuildLogLineMessage
|
|
10
|
+
from ._models import CustomDomain as CustomDomain
|
|
11
|
+
from ._models import CustomDomainRecord as CustomDomainRecord
|
|
12
|
+
from ._models import CustomDomainStatus as CustomDomainStatus
|
|
13
|
+
from ._models import DeploymentStatus as DeploymentStatus
|
|
14
|
+
from ._retry import STREAM_LOGS_MAX_RETRIES as STREAM_LOGS_MAX_RETRIES
|
|
15
|
+
from .client import APIClient as APIClient
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from fastapi_cloud_cli.utils.auth import AuthMode, delete_auth_config
|
|
7
|
+
from fastapi_cloud_cli.utils.errors import ErrorCode
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StreamLogError(Exception):
|
|
13
|
+
"""Raised when there's an error streaming logs (build or app logs)."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, *, status_code: int | None = None) -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TooManyRetriesError(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _handle_unauthorized(auth_mode: AuthMode) -> str:
|
|
25
|
+
message = "The specified token is not valid. "
|
|
26
|
+
|
|
27
|
+
if auth_mode == "user":
|
|
28
|
+
delete_auth_config()
|
|
29
|
+
|
|
30
|
+
message += "Use `fastapi login` to generate a new token."
|
|
31
|
+
else:
|
|
32
|
+
message += "Make sure to use a valid token."
|
|
33
|
+
|
|
34
|
+
return message
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_response_error_message(response: httpx.Response) -> str | None:
|
|
38
|
+
try:
|
|
39
|
+
data = response.json()
|
|
40
|
+
except (json.JSONDecodeError, httpx.ResponseNotRead):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
if not isinstance(data, dict):
|
|
44
|
+
return None # pragma: no cover
|
|
45
|
+
|
|
46
|
+
detail = data.get("detail")
|
|
47
|
+
if isinstance(detail, str):
|
|
48
|
+
return detail
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
isinstance(detail, list)
|
|
52
|
+
and detail
|
|
53
|
+
and isinstance(detail[0], dict)
|
|
54
|
+
and isinstance(message := detail[0].get("msg"), str)
|
|
55
|
+
):
|
|
56
|
+
return message.removeprefix("Value error, ")
|
|
57
|
+
|
|
58
|
+
return None # pragma: no cover
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def handle_http_error(
|
|
62
|
+
error: httpx.HTTPError,
|
|
63
|
+
default_message: str | None = None,
|
|
64
|
+
not_found_message: str | None = None,
|
|
65
|
+
auth_mode: AuthMode = "user",
|
|
66
|
+
) -> str:
|
|
67
|
+
message: str | None = None
|
|
68
|
+
|
|
69
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
70
|
+
status_code = error.response.status_code
|
|
71
|
+
|
|
72
|
+
if status_code == 422:
|
|
73
|
+
logger.debug(error.response.json())
|
|
74
|
+
message = _get_response_error_message(error.response)
|
|
75
|
+
|
|
76
|
+
elif status_code == 400:
|
|
77
|
+
message = _get_response_error_message(error.response)
|
|
78
|
+
|
|
79
|
+
elif status_code == 409:
|
|
80
|
+
message = _get_response_error_message(error.response)
|
|
81
|
+
|
|
82
|
+
elif status_code == 401:
|
|
83
|
+
message = _handle_unauthorized(auth_mode=auth_mode)
|
|
84
|
+
|
|
85
|
+
elif status_code == 403:
|
|
86
|
+
message = (
|
|
87
|
+
_get_response_error_message(error.response)
|
|
88
|
+
or "You don't have permissions for this resource"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
elif status_code == 404:
|
|
92
|
+
message = (
|
|
93
|
+
_get_response_error_message(error.response)
|
|
94
|
+
or not_found_message
|
|
95
|
+
or "Resource not found."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not message:
|
|
99
|
+
message = (
|
|
100
|
+
default_message
|
|
101
|
+
or f"Something went wrong while contacting the FastAPI Cloud server. Please try again later. \n\n{error}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return message
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_http_error_code(error: httpx.HTTPError) -> ErrorCode:
|
|
108
|
+
if isinstance(error, httpx.TimeoutException | httpx.NetworkError):
|
|
109
|
+
return "network_error"
|
|
110
|
+
|
|
111
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
112
|
+
status_code = error.response.status_code
|
|
113
|
+
|
|
114
|
+
if status_code in {400, 409, 422}:
|
|
115
|
+
return "invalid_input"
|
|
116
|
+
|
|
117
|
+
if status_code == 401:
|
|
118
|
+
return "invalid_token"
|
|
119
|
+
|
|
120
|
+
if status_code == 403:
|
|
121
|
+
return "permission_denied"
|
|
122
|
+
|
|
123
|
+
if status_code == 404:
|
|
124
|
+
return "not_found"
|
|
125
|
+
|
|
126
|
+
return "api_error"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_http_error_hint(code: ErrorCode, *, auth_mode: AuthMode = "user") -> str | None:
|
|
130
|
+
if code == "invalid_token":
|
|
131
|
+
if auth_mode == "user":
|
|
132
|
+
return "Run `fastapi cloud login` to generate a new token."
|
|
133
|
+
|
|
134
|
+
return "Make sure FASTAPI_CLOUD_TOKEN contains a valid token."
|
|
135
|
+
|
|
136
|
+
return None
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AppLogEntry(BaseModel):
|
|
8
|
+
timestamp: str
|
|
9
|
+
message: str
|
|
10
|
+
level: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BuildLogLineGeneric(BaseModel):
|
|
14
|
+
type: Literal["complete", "failed", "timeout", "heartbeat"]
|
|
15
|
+
id: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BuildLogLineMessage(BaseModel):
|
|
19
|
+
type: Literal["message"] = "message"
|
|
20
|
+
message: str
|
|
21
|
+
id: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
BuildLogLine = BuildLogLineMessage | BuildLogLineGeneric
|
|
25
|
+
BuildLogAdapter: TypeAdapter[BuildLogLine] = TypeAdapter(
|
|
26
|
+
Annotated[BuildLogLine, Field(discriminator="type")]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CustomDomainStatus(str, Enum):
|
|
31
|
+
internal_dcv_pending = "internal_dcv_pending"
|
|
32
|
+
internal_dcv_missing = "internal_dcv_missing"
|
|
33
|
+
internal_dcv_invalid = "internal_dcv_invalid"
|
|
34
|
+
internal_dcv_timeout = "internal_dcv_timeout"
|
|
35
|
+
internal_dcv_revoked = "internal_dcv_revoked"
|
|
36
|
+
external_dcv_pending = "external_dcv_pending"
|
|
37
|
+
external_dcv_proxied = "external_dcv_proxied"
|
|
38
|
+
external_dcv_secured = "external_dcv_secured"
|
|
39
|
+
external_dcv_blocked = "external_dcv_blocked"
|
|
40
|
+
external_dcv_timeout = "external_dcv_timeout"
|
|
41
|
+
origin_setup_pending = "origin_setup_pending"
|
|
42
|
+
origin_setup_missing = "origin_setup_missing"
|
|
43
|
+
origin_setup_invalid = "origin_setup_invalid"
|
|
44
|
+
origin_setup_timeout = "origin_setup_timeout"
|
|
45
|
+
origin_setup_success = "origin_setup_success"
|
|
46
|
+
origin_setup_removed = "origin_setup_removed"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CustomDomainRecord(BaseModel):
|
|
50
|
+
type: Literal["TXT", "CNAME", "A"]
|
|
51
|
+
name: str | None
|
|
52
|
+
value: str | None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CustomDomain(BaseModel):
|
|
56
|
+
id: str
|
|
57
|
+
name: str
|
|
58
|
+
status: CustomDomainStatus
|
|
59
|
+
setup_in_progress: bool
|
|
60
|
+
setup_failed: bool
|
|
61
|
+
setup_successful: bool
|
|
62
|
+
is_using_pre_validation: bool
|
|
63
|
+
dns_records: list[CustomDomainRecord]
|
|
64
|
+
created_at: str
|
|
65
|
+
updated_at: str
|
|
66
|
+
setup_started_at: str | None
|
|
67
|
+
setup_checked_at: str | None
|
|
68
|
+
app_id: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CustomDomainsAPIResponse(BaseModel):
|
|
72
|
+
data: list[CustomDomain]
|
|
73
|
+
count: int
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DeploymentStatus(str, Enum):
|
|
77
|
+
waiting_upload = "waiting_upload"
|
|
78
|
+
upload_cancelled = "upload_cancelled"
|
|
79
|
+
ready_for_build = "ready_for_build"
|
|
80
|
+
building = "building"
|
|
81
|
+
extracting = "extracting"
|
|
82
|
+
extracting_failed = "extracting_failed"
|
|
83
|
+
extracting_failed_archive_too_large = "extracting_failed_archive_too_large"
|
|
84
|
+
building_image = "building_image"
|
|
85
|
+
building_image_failed = "building_image_failed"
|
|
86
|
+
building_image_failed_timeout = "building_image_failed_timeout"
|
|
87
|
+
deploying = "deploying"
|
|
88
|
+
deploying_skipped = "deploying_skipped"
|
|
89
|
+
deploying_failed = "deploying_failed"
|
|
90
|
+
verifying = "verifying"
|
|
91
|
+
verifying_failed = "verifying_failed"
|
|
92
|
+
verification_failed_oom = "verification_failed_oom"
|
|
93
|
+
verifying_skipped = "verifying_skipped"
|
|
94
|
+
success = "success"
|
|
95
|
+
degraded_oom = "degraded_oom"
|
|
96
|
+
degraded = "degraded"
|
|
97
|
+
expired = "expired"
|
|
98
|
+
failed = "failed"
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def to_human_readable(cls, status: "DeploymentStatus") -> str:
|
|
102
|
+
return {
|
|
103
|
+
cls.waiting_upload: "Awaiting Upload",
|
|
104
|
+
cls.upload_cancelled: "Upload Cancelled",
|
|
105
|
+
cls.ready_for_build: "Build Queued",
|
|
106
|
+
cls.building: "Building",
|
|
107
|
+
cls.extracting: "Extracting Upload",
|
|
108
|
+
cls.extracting_failed: "Extraction Failed",
|
|
109
|
+
cls.extracting_failed_archive_too_large: "Archive Too Large",
|
|
110
|
+
cls.building_image: "Building Image",
|
|
111
|
+
cls.building_image_failed: "Build Failed",
|
|
112
|
+
cls.building_image_failed_timeout: "Build Timed Out",
|
|
113
|
+
cls.deploying: "Deploying Image",
|
|
114
|
+
cls.deploying_skipped: "Deployment Skipped",
|
|
115
|
+
cls.deploying_failed: "Deployment Failed",
|
|
116
|
+
cls.verifying: "Verifying Readiness",
|
|
117
|
+
cls.verifying_failed: "Verification Failed",
|
|
118
|
+
cls.verification_failed_oom: "Verification Failed (OOM)",
|
|
119
|
+
cls.verifying_skipped: "Verification Skipped",
|
|
120
|
+
cls.success: "Ready",
|
|
121
|
+
cls.degraded_oom: "Degraded (OOM)",
|
|
122
|
+
cls.degraded: "Degraded",
|
|
123
|
+
cls.expired: "Expired",
|
|
124
|
+
cls.failed: "Failed",
|
|
125
|
+
}[status]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
SUCCESSFUL_STATUSES = {DeploymentStatus.success, DeploymentStatus.verifying_skipped}
|
|
129
|
+
FAILED_STATUSES = {
|
|
130
|
+
DeploymentStatus.failed,
|
|
131
|
+
DeploymentStatus.verifying_failed,
|
|
132
|
+
DeploymentStatus.verification_failed_oom,
|
|
133
|
+
DeploymentStatus.deploying_failed,
|
|
134
|
+
DeploymentStatus.deploying_skipped,
|
|
135
|
+
DeploymentStatus.building_image_failed,
|
|
136
|
+
DeploymentStatus.building_image_failed_timeout,
|
|
137
|
+
DeploymentStatus.extracting_failed,
|
|
138
|
+
DeploymentStatus.extracting_failed_archive_too_large,
|
|
139
|
+
}
|
|
140
|
+
TERMINAL_STATUSES = SUCCESSFUL_STATUSES | FAILED_STATUSES
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Callable, Generator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from typing_extensions import ParamSpec
|
|
11
|
+
|
|
12
|
+
from ._errors import StreamLogError, TooManyRetriesError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
STREAM_LOGS_MAX_RETRIES = 3
|
|
17
|
+
STREAM_LOGS_TIMEOUT = timedelta(minutes=5)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def attempt(attempt_number: int) -> Generator[None, None, None]:
|
|
22
|
+
def _backoff() -> None:
|
|
23
|
+
backoff_seconds = min(2**attempt_number, 30)
|
|
24
|
+
logger.debug(
|
|
25
|
+
"Retrying in %ds (attempt %d)",
|
|
26
|
+
backoff_seconds,
|
|
27
|
+
attempt_number,
|
|
28
|
+
)
|
|
29
|
+
time.sleep(backoff_seconds)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
yield
|
|
33
|
+
|
|
34
|
+
except (
|
|
35
|
+
httpx.TimeoutException,
|
|
36
|
+
httpx.NetworkError,
|
|
37
|
+
httpx.RemoteProtocolError,
|
|
38
|
+
) as error:
|
|
39
|
+
logger.debug("Network error (will retry): %s", error)
|
|
40
|
+
|
|
41
|
+
_backoff()
|
|
42
|
+
|
|
43
|
+
except httpx.HTTPStatusError as error:
|
|
44
|
+
if error.response.status_code >= 500:
|
|
45
|
+
logger.debug(
|
|
46
|
+
"Server error %d (will retry): %s",
|
|
47
|
+
error.response.status_code,
|
|
48
|
+
error,
|
|
49
|
+
)
|
|
50
|
+
_backoff()
|
|
51
|
+
else:
|
|
52
|
+
# The streaming callers read the body before raising, so the
|
|
53
|
+
# server's error detail is available here.
|
|
54
|
+
raise StreamLogError(
|
|
55
|
+
f"HTTP {error.response.status_code}: {error.response.text}",
|
|
56
|
+
status_code=error.response.status_code,
|
|
57
|
+
) from error
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
P = ParamSpec("P")
|
|
61
|
+
T = TypeVar("T")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def attempts(
|
|
65
|
+
total_attempts: int = 3, timeout: timedelta = timedelta(minutes=5)
|
|
66
|
+
) -> Callable[
|
|
67
|
+
[Callable[P, Generator[T, None, None]]], Callable[P, Generator[T, None, None]]
|
|
68
|
+
]:
|
|
69
|
+
def decorator(
|
|
70
|
+
func: Callable[P, Generator[T, None, None]],
|
|
71
|
+
) -> Callable[P, Generator[T, None, None]]:
|
|
72
|
+
@wraps(func)
|
|
73
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Generator[T, None, None]:
|
|
74
|
+
start = time.monotonic()
|
|
75
|
+
|
|
76
|
+
for attempt_number in range(total_attempts):
|
|
77
|
+
if time.monotonic() - start > timeout.total_seconds():
|
|
78
|
+
raise TimeoutError(
|
|
79
|
+
f"Log streaming timed out after {timeout.total_seconds():.0f}s"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
with attempt(attempt_number):
|
|
83
|
+
yield from func(*args, **kwargs)
|
|
84
|
+
# If we get here without exception, the generator completed successfully
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
raise TooManyRetriesError(f"Failed after {total_attempts} attempts")
|
|
88
|
+
|
|
89
|
+
return wrapper
|
|
90
|
+
|
|
91
|
+
return decorator
|