fastapi-cloud-cli 0.7.0__tar.gz → 0.9.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.
Files changed (80) hide show
  1. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/PKG-INFO +3 -4
  2. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/pyproject.toml +4 -5
  3. fastapi_cloud_cli-0.9.0/src/fastapi_cloud_cli/__init__.py +1 -0
  4. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/deploy.py +110 -45
  5. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/env.py +13 -10
  6. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/login.py +7 -13
  7. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/whoami.py +8 -2
  8. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/api.py +10 -10
  9. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/apps.py +2 -4
  10. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/auth.py +35 -17
  11. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/cli.py +14 -4
  12. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/sentry.py +4 -2
  13. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/conftest.py +1 -1
  14. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_archive.py +1 -0
  15. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_auth.py +32 -32
  16. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_deploy.py +399 -55
  17. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_whoami.py +7 -0
  18. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_env_delete.py +3 -2
  19. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_env_set.py +3 -2
  20. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/utils.py +4 -3
  21. fastapi_cloud_cli-0.7.0/src/fastapi_cloud_cli/__init__.py +0 -1
  22. fastapi_cloud_cli-0.7.0/src/fastapi_cloud_cli/utils/pydantic_compat.py +0 -72
  23. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/LICENSE +0 -0
  24. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/README.md +0 -0
  25. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/format.sh +0 -0
  26. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/lint.sh +0 -0
  27. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/test-cov-html.sh +0 -0
  28. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/scripts/test.sh +0 -0
  29. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/__main__.py +0 -0
  30. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/cli.py +0 -0
  31. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/__init__.py +0 -0
  32. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/logout.py +0 -0
  33. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/commands/unlink.py +0 -0
  34. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/config.py +0 -0
  35. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/logging.py +0 -0
  36. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/py.typed +0 -0
  37. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/__init__.py +0 -0
  38. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/config.py +0 -0
  39. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/src/fastapi_cloud_cli/utils/env.py +0 -0
  40. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/__init__.py +0 -0
  41. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/broken_package/mod/__init__.py +0 -0
  42. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/broken_package/mod/app.py +0 -0
  43. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/broken_package/utils.py +0 -0
  44. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_api/api.py +0 -0
  45. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app/api.py +0 -0
  46. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app/app.py +0 -0
  47. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_api/app/__init__.py +0 -0
  48. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_api/app/api.py +0 -0
  49. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_app/app/__init__.py +0 -0
  50. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_app/app/api.py +0 -0
  51. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_app/app/app.py +0 -0
  52. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/__init__.py +0 -0
  53. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/api.py +0 -0
  54. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/app.py +0 -0
  55. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_main/app/main.py +0 -0
  56. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_non_default/app/__init__.py +0 -0
  57. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_app_dir_non_default/app/nondefault.py +0 -0
  58. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_main/api.py +0 -0
  59. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_main/app.py +0 -0
  60. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/default_main/main.py +0 -0
  61. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/default_files/non_default/nonstandard.py +0 -0
  62. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/__init__.py +0 -0
  63. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/core/__init__.py +0 -0
  64. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/core/utils.py +0 -0
  65. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/__init__.py +0 -0
  66. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/api.py +0 -0
  67. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/app.py +0 -0
  68. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/package/mod/other.py +0 -0
  69. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/single_file_api.py +0 -0
  70. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/single_file_app.py +0 -0
  71. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/assets/single_file_other.py +0 -0
  72. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_api_client.py +0 -0
  73. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli.py +0 -0
  74. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_login.py +0 -0
  75. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_logout.py +0 -0
  76. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_cli_unlink.py +0 -0
  77. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_config.py +0 -0
  78. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_deploy_utils.py +0 -0
  79. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_env_list.py +0 -0
  80. {fastapi_cloud_cli-0.7.0 → fastapi_cloud_cli-0.9.0}/tests/test_sentry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-cloud-cli
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -19,7 +19,6 @@ Classifier: Framework :: FastAPI
19
19
  Classifier: Intended Audience :: Developers
20
20
  Classifier: License :: OSI Approved :: MIT License
21
21
  Classifier: Programming Language :: Python :: 3 :: Only
22
- Classifier: Programming Language :: Python :: 3.8
23
22
  Classifier: Programming Language :: Python :: 3.9
24
23
  Classifier: Programming Language :: Python :: 3.10
25
24
  Classifier: Programming Language :: Python :: 3.11
@@ -30,13 +29,13 @@ Project-URL: Documentation, https://fastapi.tiangolo.com/fastapi-cloud-cli/
30
29
  Project-URL: Repository, https://github.com/fastapilabs/fastapi-cloud-cli
31
30
  Project-URL: Issues, https://github.com/fastapilabs/fastapi-cloud-cli/issues
32
31
  Project-URL: Changelog, https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-notes.md
33
- Requires-Python: >=3.8
32
+ Requires-Python: >=3.9
34
33
  Requires-Dist: typer>=0.12.3
35
34
  Requires-Dist: uvicorn[standard]>=0.15.0
36
35
  Requires-Dist: rignore>=0.5.1
37
36
  Requires-Dist: httpx>=0.27.0
38
37
  Requires-Dist: rich-toolkit>=0.14.5
39
- Requires-Dist: pydantic[email]>=1.6.1
38
+ Requires-Dist: pydantic[email]>=2.0
40
39
  Requires-Dist: sentry-sdk>=2.20.0
41
40
  Requires-Dist: fastar>=0.8.0
42
41
  Provides-Extra: standard
@@ -5,7 +5,7 @@ description = "Deploy and manage FastAPI Cloud apps from the command line 🚀"
5
5
  authors = [
6
6
  { name = "Patrick Arminio", email = "patrick@fastapilabs.com" },
7
7
  ]
8
- requires-python = ">=3.8"
8
+ requires-python = ">=3.9"
9
9
  readme = "README.md"
10
10
  classifiers = [
11
11
  "Intended Audience :: Information Technology",
@@ -23,7 +23,6 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  "License :: OSI Approved :: MIT License",
25
25
  "Programming Language :: Python :: 3 :: Only",
26
- "Programming Language :: Python :: 3.8",
27
26
  "Programming Language :: Python :: 3.9",
28
27
  "Programming Language :: Python :: 3.10",
29
28
  "Programming Language :: Python :: 3.11",
@@ -36,11 +35,11 @@ dependencies = [
36
35
  "rignore >= 0.5.1",
37
36
  "httpx >= 0.27.0",
38
37
  "rich-toolkit >= 0.14.5",
39
- "pydantic[email] >= 1.6.1",
38
+ "pydantic[email] >= 2.0",
40
39
  "sentry-sdk >= 2.20.0",
41
40
  "fastar >= 0.8.0",
42
41
  ]
43
- version = "0.7.0"
42
+ version = "0.9.0"
44
43
 
45
44
  [project.license]
46
45
  text = "MIT"
@@ -59,7 +58,7 @@ Changelog = "https://github.com/fastapilabs/fastapi-cloud-cli/blob/main/release-
59
58
 
60
59
  [dependency-groups]
61
60
  dev = [
62
- "pre-commit>=2.17.0,<5.0.0",
61
+ "prek>=0.2.24,<1.0.0",
63
62
  "pytest>=4.4.0,<9.0.0",
64
63
  "coverage[toml]>=6.2,<8.0",
65
64
  "mypy==1.14.1",
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -7,28 +7,22 @@ from enum import Enum
7
7
  from itertools import cycle
8
8
  from pathlib import Path
9
9
  from textwrap import dedent
10
- from typing import Any, Dict, List, Optional, Union
10
+ from typing import Annotated, Any, Optional, Union
11
11
 
12
12
  import fastar
13
13
  import rignore
14
14
  import typer
15
15
  from httpx import Client
16
- from pydantic import BaseModel, EmailStr, ValidationError
16
+ from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
17
17
  from rich.text import Text
18
18
  from rich_toolkit import RichToolkit
19
19
  from rich_toolkit.menu import Option
20
- from typing_extensions import Annotated
21
20
 
22
21
  from fastapi_cloud_cli.commands.login import login
23
22
  from fastapi_cloud_cli.utils.api import APIClient, BuildLogError, TooManyRetriesError
24
23
  from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
25
- from fastapi_cloud_cli.utils.auth import is_logged_in
24
+ from fastapi_cloud_cli.utils.auth import Identity
26
25
  from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
27
- from fastapi_cloud_cli.utils.pydantic_compat import (
28
- TypeAdapter,
29
- model_dump,
30
- model_validate,
31
- )
32
26
 
33
27
  logger = logging.getLogger(__name__)
34
28
 
@@ -57,6 +51,7 @@ def _should_exclude_entry(path: Path) -> bool:
57
51
  "__pycache__",
58
52
  ".mypy_cache",
59
53
  ".pytest_cache",
54
+ ".git",
60
55
  ".gitignore",
61
56
  ".fastapicloudignore",
62
57
  ]
@@ -102,14 +97,14 @@ class Team(BaseModel):
102
97
  name: str
103
98
 
104
99
 
105
- def _get_teams() -> List[Team]:
100
+ def _get_teams() -> list[Team]:
106
101
  with APIClient() as client:
107
102
  response = client.get("/teams/")
108
103
  response.raise_for_status()
109
104
 
110
105
  data = response.json()["data"]
111
106
 
112
- return [model_validate(Team, team) for team in data]
107
+ return [Team.model_validate(team) for team in data]
113
108
 
114
109
 
115
110
  class AppResponse(BaseModel):
@@ -126,7 +121,7 @@ def _create_app(team_id: str, app_name: str) -> AppResponse:
126
121
 
127
122
  response.raise_for_status()
128
123
 
129
- return model_validate(AppResponse, response.json())
124
+ return AppResponse.model_validate(response.json())
130
125
 
131
126
 
132
127
  class DeploymentStatus(str, Enum):
@@ -179,12 +174,12 @@ def _create_deployment(app_id: str) -> CreateDeploymentResponse:
179
174
  response = client.post(f"/apps/{app_id}/deployments/")
180
175
  response.raise_for_status()
181
176
 
182
- return model_validate(CreateDeploymentResponse, response.json())
177
+ return CreateDeploymentResponse.model_validate(response.json())
183
178
 
184
179
 
185
180
  class RequestUploadResponse(BaseModel):
186
181
  url: str
187
- fields: Dict[str, str]
182
+ fields: dict[str, str]
188
183
 
189
184
 
190
185
  def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
@@ -204,7 +199,7 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
204
199
  response = fastapi_client.post(f"/deployments/{deployment_id}/upload")
205
200
  response.raise_for_status()
206
201
 
207
- upload_data = model_validate(RequestUploadResponse, response.json())
202
+ upload_data = RequestUploadResponse.model_validate(response.json())
208
203
  logger.debug("Received upload URL: %s", upload_data.url)
209
204
 
210
205
  logger.debug("Starting file upload to S3")
@@ -239,17 +234,17 @@ def _get_app(app_slug: str) -> Optional[AppResponse]:
239
234
 
240
235
  data = response.json()
241
236
 
242
- return model_validate(AppResponse, data)
237
+ return AppResponse.model_validate(data)
243
238
 
244
239
 
245
- def _get_apps(team_id: str) -> List[AppResponse]:
240
+ def _get_apps(team_id: str) -> list[AppResponse]:
246
241
  with APIClient() as client:
247
242
  response = client.get("/apps/", params={"team_id": team_id})
248
243
  response.raise_for_status()
249
244
 
250
245
  data = response.json()["data"]
251
246
 
252
- return [model_validate(AppResponse, app) for app in data]
247
+ return [AppResponse.model_validate(app) for app in data]
253
248
 
254
249
 
255
250
  WAITING_MESSAGES = [
@@ -300,6 +295,8 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
300
295
 
301
296
  toolkit.print_line()
302
297
 
298
+ selected_app: Optional[AppResponse] = None
299
+
303
300
  if not create_new_app:
304
301
  with toolkit.progress("Fetching apps...") as progress:
305
302
  with handle_http_errors(
@@ -316,18 +313,45 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
316
313
 
317
314
  raise typer.Exit(1)
318
315
 
319
- app = toolkit.ask(
316
+ selected_app = toolkit.ask(
320
317
  "Select the app you want to deploy to:",
321
318
  options=[Option({"name": app.slug, "value": app}) for app in apps],
322
319
  )
323
- else:
324
- app_name = toolkit.input(
320
+
321
+ app_name = (
322
+ selected_app.slug
323
+ if selected_app
324
+ else toolkit.input(
325
325
  title="What's your app name?",
326
326
  default=_get_app_name(path_to_deploy),
327
327
  )
328
+ )
328
329
 
329
- toolkit.print_line()
330
+ toolkit.print_line()
331
+
332
+ toolkit.print("Deployment configuration:", tag="summary")
333
+ toolkit.print_line()
334
+ toolkit.print(f"Team: [bold]{team.name}[/bold]")
335
+ toolkit.print(f"App name: [bold]{app_name}[/bold]")
336
+ toolkit.print_line()
337
+
338
+ choice = toolkit.ask(
339
+ "Does everything look right?",
340
+ tag="confirm",
341
+ options=[
342
+ Option({"name": "Yes, start the deployment!", "value": "deploy"}),
343
+ Option({"name": "No, let me start over", "value": "cancel"}),
344
+ ],
345
+ )
346
+ toolkit.print_line()
330
347
 
348
+ if choice == "cancel":
349
+ toolkit.print("Deployment cancelled.")
350
+ raise typer.Exit(0)
351
+
352
+ if selected_app: # pragma: no cover
353
+ app = selected_app
354
+ else:
331
355
  with toolkit.progress(title="Creating app...") as progress:
332
356
  with handle_http_errors(progress):
333
357
  app = _create_app(team.id, app_name)
@@ -363,9 +387,12 @@ def _wait_for_deployment(
363
387
 
364
388
  last_message_changed_at = time.monotonic()
365
389
 
366
- with toolkit.progress(
367
- next(messages), inline_logs=True, lines_to_show=20
368
- ) as progress, APIClient() as client:
390
+ with (
391
+ toolkit.progress(
392
+ next(messages), inline_logs=True, lines_to_show=20
393
+ ) as progress,
394
+ APIClient() as client,
395
+ ):
369
396
  try:
370
397
  for log in client.stream_build_logs(deployment.id):
371
398
  time_elapsed = time.monotonic() - started_at
@@ -376,13 +403,13 @@ def _wait_for_deployment(
376
403
  if log.type == "complete":
377
404
  progress.log("")
378
405
  progress.log(
379
- f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
406
+ f"You can also check the app logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
380
407
  )
381
408
 
382
409
  progress.log("")
383
410
 
384
411
  progress.log(
385
- f"You can also check the app logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
412
+ f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
386
413
  )
387
414
 
388
415
  break
@@ -432,7 +459,7 @@ def _send_waitlist_form(
432
459
  with toolkit.progress("Sending your request...") as progress:
433
460
  with APIClient() as client:
434
461
  with handle_http_errors(progress):
435
- response = client.post("/users/waiting-list", json=model_dump(result))
462
+ response = client.post("/users/waiting-list", json=result.model_dump())
436
463
 
437
464
  response.raise_for_status()
438
465
 
@@ -457,7 +484,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
457
484
 
458
485
  toolkit.print_line()
459
486
 
460
- result = model_validate(SignupToWaitingList, {"email": email})
487
+ result = SignupToWaitingList.model_validate({"email": email})
461
488
 
462
489
  if toolkit.confirm(
463
490
  "Do you want to get access faster by giving us more information?",
@@ -481,8 +508,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
481
508
  result = form.run() # type: ignore
482
509
 
483
510
  try:
484
- result = model_validate(
485
- SignupToWaitingList,
511
+ result = SignupToWaitingList.model_validate(
486
512
  {
487
513
  "email": email,
488
514
  **result, # type: ignore
@@ -531,15 +557,27 @@ def deploy(
531
557
  skip_wait: Annotated[
532
558
  bool, typer.Option("--no-wait", help="Skip waiting for deployment status")
533
559
  ] = False,
560
+ provided_app_id: Annotated[
561
+ Union[str, None],
562
+ typer.Option(
563
+ "--app-id",
564
+ help="Application ID to deploy to",
565
+ envvar="FASTAPI_CLOUD_APP_ID",
566
+ ),
567
+ ] = None,
534
568
  ) -> Any:
535
569
  """
536
570
  Deploy a [bold]FastAPI[/bold] app to FastAPI Cloud. 🚀
537
571
  """
538
572
  logger.debug("Deploy command started")
539
- logger.debug("Deploy path: %s, skip_wait: %s", path, skip_wait)
573
+ logger.debug(
574
+ "Deploy path: %s, skip_wait: %s, app_id: %s", path, skip_wait, provided_app_id
575
+ )
576
+
577
+ identity = Identity()
540
578
 
541
579
  with get_rich_toolkit() as toolkit:
542
- if not is_logged_in():
580
+ if not identity.is_logged_in():
543
581
  logger.debug("User not logged in, prompting for login or waitlist")
544
582
 
545
583
  toolkit.print_title("Welcome to FastAPI Cloud!", tag="FastAPI")
@@ -576,19 +614,43 @@ def deploy(
576
614
 
577
615
  app_config = get_app_config(path_to_deploy)
578
616
 
579
- if not app_config:
617
+ if app_config and provided_app_id and app_config.app_id != provided_app_id:
618
+ toolkit.print(
619
+ f"[error]Error: Provided app ID ({provided_app_id}) does not match the local "
620
+ f"config ({app_config.app_id}).[/]"
621
+ )
622
+ toolkit.print_line()
623
+ toolkit.print(
624
+ "Run [bold]fastapi cloud unlink[/] to remove the local config, "
625
+ "or remove --app-id / unset FASTAPI_CLOUD_APP_ID to use the configured app.",
626
+ tag="tip",
627
+ )
628
+
629
+ raise typer.Exit(1) from None
630
+
631
+ if provided_app_id:
632
+ target_app_id = provided_app_id
633
+ elif app_config:
634
+ target_app_id = app_config.app_id
635
+ else:
580
636
  logger.debug("No app config found, configuring new app")
637
+
581
638
  app_config = _configure_app(toolkit, path_to_deploy=path_to_deploy)
582
639
  toolkit.print_line()
640
+
641
+ target_app_id = app_config.app_id
642
+
643
+ if provided_app_id:
644
+ toolkit.print(f"Deploying to app [blue]{target_app_id}[/blue]...")
583
645
  else:
584
- logger.debug("Existing app config found, proceeding with deployment")
585
646
  toolkit.print("Deploying app...")
586
- toolkit.print_line()
647
+
648
+ toolkit.print_line()
587
649
 
588
650
  with toolkit.progress("Checking app...", transient=True) as progress:
589
651
  with handle_http_errors(progress):
590
- logger.debug("Checking app with ID: %s", app_config.app_id)
591
- app = _get_app(app_config.app_id)
652
+ logger.debug("Checking app with ID: %s", target_app_id)
653
+ app = _get_app(target_app_id)
592
654
 
593
655
  if not app:
594
656
  logger.debug("App not found in API")
@@ -598,10 +660,12 @@ def deploy(
598
660
 
599
661
  if not app:
600
662
  toolkit.print_line()
601
- toolkit.print(
602
- "If you deleted this app, you can run [bold]fastapi cloud unlink[/] to unlink the local configuration.",
603
- tag="tip",
604
- )
663
+
664
+ if not provided_app_id:
665
+ toolkit.print(
666
+ "If you deleted this app, you can run [bold]fastapi cloud unlink[/] to unlink the local configuration.",
667
+ tag="tip",
668
+ )
605
669
  raise typer.Exit(1)
606
670
 
607
671
  with tempfile.TemporaryDirectory() as temp_dir:
@@ -609,9 +673,10 @@ def deploy(
609
673
  archive_path = Path(temp_dir) / "archive.tar"
610
674
  archive(path or Path.cwd(), archive_path)
611
675
 
612
- with toolkit.progress(
613
- title="Creating deployment"
614
- ) as progress, handle_http_errors(progress):
676
+ with (
677
+ toolkit.progress(title="Creating deployment") as progress,
678
+ handle_http_errors(progress),
679
+ ):
615
680
  logger.debug("Creating deployment for app: %s", app.id)
616
681
  deployment = _create_deployment(app.id)
617
682
 
@@ -1,17 +1,15 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Any, List, Union
3
+ from typing import Annotated, Any, Union
4
4
 
5
5
  import typer
6
6
  from pydantic import BaseModel
7
- from typing_extensions import Annotated
8
7
 
9
8
  from fastapi_cloud_cli.utils.api import APIClient
10
9
  from fastapi_cloud_cli.utils.apps import get_app_config
11
- from fastapi_cloud_cli.utils.auth import is_logged_in
10
+ from fastapi_cloud_cli.utils.auth import Identity
12
11
  from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
13
12
  from fastapi_cloud_cli.utils.env import validate_environment_variable_name
14
- from fastapi_cloud_cli.utils.pydantic_compat import model_validate
15
13
 
16
14
  logger = logging.getLogger(__name__)
17
15
 
@@ -22,7 +20,7 @@ class EnvironmentVariable(BaseModel):
22
20
 
23
21
 
24
22
  class EnvironmentVariableResponse(BaseModel):
25
- data: List[EnvironmentVariable]
23
+ data: list[EnvironmentVariable]
26
24
 
27
25
 
28
26
  def _get_environment_variables(app_id: str) -> EnvironmentVariableResponse:
@@ -30,7 +28,7 @@ def _get_environment_variables(app_id: str) -> EnvironmentVariableResponse:
30
28
  response = client.get(f"/apps/{app_id}/environment-variables/")
31
29
  response.raise_for_status()
32
30
 
33
- return model_validate(EnvironmentVariableResponse, response.json())
31
+ return EnvironmentVariableResponse.model_validate(response.json())
34
32
 
35
33
 
36
34
  def _delete_environment_variable(app_id: str, name: str) -> bool:
@@ -72,8 +70,10 @@ def list(
72
70
  List the environment variables for the app.
73
71
  """
74
72
 
73
+ identity = Identity()
74
+
75
75
  with get_rich_toolkit(minimal=True) as toolkit:
76
- if not is_logged_in():
76
+ if not identity.is_logged_in():
77
77
  toolkit.print(
78
78
  "No credentials found. Use [blue]`fastapi login`[/] to login.",
79
79
  tag="auth",
@@ -125,9 +125,10 @@ def delete(
125
125
  Delete an environment variable from the app.
126
126
  """
127
127
 
128
+ identity = Identity()
129
+
128
130
  with get_rich_toolkit(minimal=True) as toolkit:
129
- # TODO: maybe this logic can be extracted to a function
130
- if not is_logged_in():
131
+ if not identity.is_logged_in():
131
132
  toolkit.print(
132
133
  "No credentials found. Use [blue]`fastapi login`[/] to login.",
133
134
  tag="auth",
@@ -210,8 +211,10 @@ def set(
210
211
  Set an environment variable for the app.
211
212
  """
212
213
 
214
+ identity = Identity()
215
+
213
216
  with get_rich_toolkit(minimal=True) as toolkit:
214
- if not is_logged_in():
217
+ if not identity.is_logged_in():
215
218
  toolkit.print(
216
219
  "No credentials found. Use [blue]`fastapi login`[/] to login.",
217
220
  tag="auth",
@@ -8,15 +8,8 @@ from pydantic import BaseModel
8
8
 
9
9
  from fastapi_cloud_cli.config import Settings
10
10
  from fastapi_cloud_cli.utils.api import APIClient
11
- from fastapi_cloud_cli.utils.auth import (
12
- AuthConfig,
13
- get_auth_token,
14
- is_logged_in,
15
- is_token_expired,
16
- write_auth_config,
17
- )
11
+ from fastapi_cloud_cli.utils.auth import AuthConfig, Identity, write_auth_config
18
12
  from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
19
- from fastapi_cloud_cli.utils.pydantic_compat import model_validate_json
20
13
 
21
14
  logger = logging.getLogger(__name__)
22
15
 
@@ -44,7 +37,7 @@ def _start_device_authorization(
44
37
 
45
38
  response.raise_for_status()
46
39
 
47
- return model_validate_json(AuthorizationData, response.text)
40
+ return AuthorizationData.model_validate_json(response.text)
48
41
 
49
42
 
50
43
  def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -> str:
@@ -74,7 +67,7 @@ def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -
74
67
 
75
68
  time.sleep(interval)
76
69
 
77
- response_data = model_validate_json(TokenResponse, response.text)
70
+ response_data = TokenResponse.model_validate_json(response.text)
78
71
 
79
72
  return response_data.access_token
80
73
 
@@ -83,13 +76,14 @@ def login() -> Any:
83
76
  """
84
77
  Login to FastAPI Cloud. 🚀
85
78
  """
86
- token = get_auth_token()
87
- if token is not None and is_token_expired(token):
79
+ identity = Identity()
80
+
81
+ if identity.is_expired():
88
82
  with get_rich_toolkit(minimal=True) as toolkit:
89
83
  toolkit.print("Your session has expired. Logging in again...")
90
84
  toolkit.print_line()
91
85
 
92
- if is_logged_in():
86
+ if identity.is_logged_in():
93
87
  with get_rich_toolkit(minimal=True) as toolkit:
94
88
  toolkit.print("You are already logged in.")
95
89
  toolkit.print(
@@ -5,14 +5,20 @@ from rich import print
5
5
  from rich_toolkit.progress import Progress
6
6
 
7
7
  from fastapi_cloud_cli.utils.api import APIClient
8
- from fastapi_cloud_cli.utils.auth import is_logged_in
8
+ from fastapi_cloud_cli.utils.auth import Identity
9
9
  from fastapi_cloud_cli.utils.cli import handle_http_errors
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
14
  def whoami() -> Any:
15
- if not is_logged_in():
15
+ identity = Identity()
16
+
17
+ if identity.auth_mode == "token":
18
+ print("⚡ [bold]Using API token from environment variable[/bold]")
19
+ return
20
+
21
+ if not identity.is_logged_in():
16
22
  print("No credentials found. Use [blue]`fastapi login`[/] to login.")
17
23
  return
18
24
 
@@ -1,12 +1,13 @@
1
1
  import json
2
2
  import logging
3
3
  import time
4
+ from collections.abc import Generator
4
5
  from contextlib import contextmanager
5
6
  from datetime import timedelta
6
7
  from functools import wraps
7
8
  from typing import (
9
+ Annotated,
8
10
  Callable,
9
- Generator,
10
11
  Literal,
11
12
  Optional,
12
13
  TypeVar,
@@ -14,13 +15,13 @@ from typing import (
14
15
  )
15
16
 
16
17
  import httpx
17
- from pydantic import BaseModel, Field, ValidationError
18
- from typing_extensions import Annotated, ParamSpec
18
+ from pydantic import BaseModel, Field, TypeAdapter, ValidationError
19
+ from typing_extensions import ParamSpec
19
20
 
20
21
  from fastapi_cloud_cli import __version__
21
22
  from fastapi_cloud_cli.config import Settings
22
- from fastapi_cloud_cli.utils.auth import get_auth_token
23
- from fastapi_cloud_cli.utils.pydantic_compat import TypeAdapter
23
+
24
+ from .auth import Identity
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
@@ -48,8 +49,8 @@ class BuildLogLineMessage(BaseModel):
48
49
 
49
50
 
50
51
  BuildLogLine = Union[BuildLogLineMessage, BuildLogLineGeneric]
51
- BuildLogAdapter = TypeAdapter[BuildLogLine](
52
- Annotated[BuildLogLine, Field(discriminator="type")] # type: ignore
52
+ BuildLogAdapter: TypeAdapter[BuildLogLine] = TypeAdapter(
53
+ Annotated[BuildLogLine, Field(discriminator="type")]
53
54
  )
54
55
 
55
56
 
@@ -132,14 +133,13 @@ def attempts(
132
133
  class APIClient(httpx.Client):
133
134
  def __init__(self) -> None:
134
135
  settings = Settings.get()
135
-
136
- token = get_auth_token()
136
+ identity = Identity()
137
137
 
138
138
  super().__init__(
139
139
  base_url=settings.base_api_url,
140
140
  timeout=httpx.Timeout(20),
141
141
  headers={
142
- "Authorization": f"Bearer {token}",
142
+ "Authorization": f"Bearer {identity.token}",
143
143
  "User-Agent": f"fastapi-cloud-cli/{__version__}",
144
144
  },
145
145
  )
@@ -4,8 +4,6 @@ from typing import Optional
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
- from fastapi_cloud_cli.utils.pydantic_compat import model_dump_json, model_validate_json
8
-
9
7
  logger = logging.getLogger("fastapi_cli")
10
8
 
11
9
 
@@ -23,7 +21,7 @@ def get_app_config(path_to_deploy: Path) -> Optional[AppConfig]:
23
21
  return None
24
22
 
25
23
  logger.debug("App config loaded successfully")
26
- return model_validate_json(AppConfig, config_path.read_text(encoding="utf-8"))
24
+ return AppConfig.model_validate_json(config_path.read_text(encoding="utf-8"))
27
25
 
28
26
 
29
27
  README = """
@@ -52,7 +50,7 @@ def write_app_config(path_to_deploy: Path, app_config: AppConfig) -> None:
52
50
  config_path.parent.mkdir(parents=True, exist_ok=True)
53
51
 
54
52
  config_path.write_text(
55
- model_dump_json(app_config),
53
+ app_config.model_dump_json(),
56
54
  encoding="utf-8",
57
55
  )
58
56
  readme_path.write_text(README, encoding="utf-8")