Skip to content

Commit b15f095

Browse files
authored
Merge branch 'main' into fix-logout-message
2 parents 7221361 + 66dbdf5 commit b15f095

File tree

15 files changed

+951
-73
lines changed

15 files changed

+951
-73
lines changed

.github/dependabot.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ updates:
77
interval: "daily"
88
commit-message:
99
prefix:
10+
labels: [dependencies, internal]
1011
# Python
1112
- package-ecosystem: "pip"
1213
directory: "/"
1314
schedule:
1415
interval: "daily"
1516
commit-message:
1617
prefix:
18+
labels: [dependencies, internal]

.github/workflows/test.yml

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,23 @@ on:
2323

2424
jobs:
2525
test:
26-
runs-on: ubuntu-latest
2726
strategy:
2827
matrix:
29-
python-version:
30-
- "3.8"
31-
- "3.9"
32-
- "3.10"
33-
- "3.11"
34-
- "3.12"
28+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
29+
pydantic-version: ["v2"]
30+
include:
31+
- python-version: "3.8"
32+
pydantic-version: "v1"
33+
- python-version: "3.9"
34+
pydantic-version: "v1"
35+
- python-version: "3.10"
36+
pydantic-version: "v1"
37+
- python-version: "3.11"
38+
pydantic-version: "v1"
39+
- python-version: "3.12"
40+
pydantic-version: "v1"
3541
fail-fast: false
42+
runs-on: ubuntu-latest
3643
steps:
3744
- name: Dump GitHub context
3845
env:
@@ -50,7 +57,7 @@ jobs:
5057
id: cache
5158
with:
5259
path: ${{ env.pythonLocation }}
53-
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}
60+
key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-{{ matrix.pydantic-version }}
5461
# Allow debugging with tmate
5562
- name: Setup tmate session
5663
uses: mxschmitt/action-tmate@v3
@@ -60,6 +67,9 @@ jobs:
6067
- name: Install Dependencies
6168
if: steps.cache.outputs.cache-hit != 'true'
6269
run: pip install -r requirements-tests.txt
70+
- name: Install Pydantic v1
71+
if: matrix.pydantic-version == 'v1'
72+
run: pip install "pydantic<2.0.0"
6373
- name: Lint
6474
run: bash scripts/lint.sh
6575
- run: mkdir coverage
@@ -71,7 +81,7 @@ jobs:
7181
- name: Store coverage files
7282
uses: actions/upload-artifact@v4
7383
with:
74-
name: coverage-${{ matrix.python-version }}
84+
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
7585
path: coverage
7686
include-hidden-files: true
7787

release-notes.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
## Latest Changes
44

5+
### Refactors
6+
7+
* ♻️ Improve waitlist success command. PR [#117](https://github.com/fastapilabs/fastapi-cloud-cli/pull/117) by [@patrick91](https://github.com/patrick91).
8+
9+
## 0.5.0
10+
11+
### Features
12+
13+
* ✨ Resume build log stream if interrupted. PR [#109](https://github.com/fastapilabs/fastapi-cloud-cli/pull/109) by [@buurro](https://github.com/buurro).
14+
15+
### Internal
16+
17+
* 👷 Add support for Pydantic v1. PR [#116](https://github.com/fastapilabs/fastapi-cloud-cli/pull/116) by [@patrick91](https://github.com/patrick91).
18+
* 🔧 Add labels to Dependabot updates. PR [#113](https://github.com/fastapilabs/fastapi-cloud-cli/pull/113) by [@alejsdev](https://github.com/alejsdev).
19+
20+
## 0.4.0
21+
522
### Features
623

724
* ✨ Add fastapi cloud sub-command. PR [#104](https://github.com/fastapilabs/fastapi-cloud-cli/pull/104) by [@buurro](https://github.com/buurro).

requirements-tests.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ coverage[toml] >=6.2,<8.0
55
mypy ==1.14.1
66
ruff ==0.13.0
77
respx ==0.22.0
8+
time-machine ==2.15.0

src/fastapi_cloud_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.1"
1+
__version__ = "0.5.0"

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
import contextlib
2-
import json
32
import logging
43
import subprocess
54
import tempfile
65
import time
76
from enum import Enum
87
from itertools import cycle
98
from pathlib import Path
10-
from typing import Any, Dict, Generator, List, Optional, Union
9+
from typing import Any, Dict, List, Optional, Union
1110

1211
import fastar
1312
import rignore
1413
import typer
1514
from httpx import Client
16-
from pydantic import BaseModel, EmailStr, TypeAdapter, ValidationError
15+
from pydantic import BaseModel, EmailStr, ValidationError
1716
from rich.text import Text
1817
from rich_toolkit import RichToolkit
1918
from rich_toolkit.menu import Option
2019
from typing_extensions import Annotated
2120

2221
from fastapi_cloud_cli.commands.login import login
23-
from fastapi_cloud_cli.utils.api import APIClient
22+
from fastapi_cloud_cli.utils.api import APIClient, BuildLogError, TooManyRetriesError
2423
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
2524
from fastapi_cloud_cli.utils.auth import is_logged_in
2625
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
26+
from fastapi_cloud_cli.utils.pydantic_compat import (
27+
TypeAdapter,
28+
model_dump,
29+
model_validate,
30+
)
2731

2832
logger = logging.getLogger(__name__)
2933

@@ -91,7 +95,7 @@ def _get_teams() -> List[Team]:
9195

9296
data = response.json()["data"]
9397

94-
return [Team.model_validate(team) for team in data]
98+
return [model_validate(Team, team) for team in data]
9599

96100

97101
class AppResponse(BaseModel):
@@ -108,7 +112,7 @@ def _create_app(team_id: str, app_name: str) -> AppResponse:
108112

109113
response.raise_for_status()
110114

111-
return AppResponse.model_validate(response.json())
115+
return model_validate(AppResponse, response.json())
112116

113117

114118
class DeploymentStatus(str, Enum):
@@ -161,7 +165,7 @@ def _create_deployment(app_id: str) -> CreateDeploymentResponse:
161165
response = client.post(f"/apps/{app_id}/deployments/")
162166
response.raise_for_status()
163167

164-
return CreateDeploymentResponse.model_validate(response.json())
168+
return model_validate(CreateDeploymentResponse, response.json())
165169

166170

167171
class RequestUploadResponse(BaseModel):
@@ -186,7 +190,7 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
186190
response = fastapi_client.post(f"/deployments/{deployment_id}/upload")
187191
response.raise_for_status()
188192

189-
upload_data = RequestUploadResponse.model_validate(response.json())
193+
upload_data = model_validate(RequestUploadResponse, response.json())
190194
logger.debug("Received upload URL: %s", upload_data.url)
191195

192196
# Upload the archive
@@ -221,7 +225,7 @@ def _get_app(app_slug: str) -> Optional[AppResponse]:
221225

222226
data = response.json()
223227

224-
return AppResponse.model_validate(data)
228+
return model_validate(AppResponse, data)
225229

226230

227231
def _get_apps(team_id: str) -> List[AppResponse]:
@@ -231,24 +235,14 @@ def _get_apps(team_id: str) -> List[AppResponse]:
231235

232236
data = response.json()["data"]
233237

234-
return [AppResponse.model_validate(app) for app in data]
235-
236-
237-
def _stream_build_logs(deployment_id: str) -> Generator[str, None, None]:
238-
with APIClient() as client:
239-
with client.stream(
240-
"GET", f"/deployments/{deployment_id}/build-logs", timeout=60
241-
) as response:
242-
response.raise_for_status()
243-
244-
yield from response.iter_lines()
238+
return [model_validate(AppResponse, app) for app in data]
245239

246240

247241
WAITING_MESSAGES = [
248242
"🚀 Preparing for liftoff! Almost there...",
249243
"👹 Sneaking past the dependency gremlins... Don't wake them up!",
250244
"🤏 Squishing code into a tiny digital sandwich. Nom nom nom.",
251-
"📉 Server space running low. Time to delete those cat videos?",
245+
"🐱 Removing cat videos from our servers to free up space.",
252246
"🐢 Uploading at blazing speeds of 1 byte per hour. Patience, young padawan.",
253247
"🔌 Connecting to server... Please stand by while we argue with the firewall.",
254248
"💥 Oops! We've angered the Python God. Sacrificing a rubber duck to appease it.",
@@ -358,17 +352,15 @@ def _wait_for_deployment(
358352

359353
with toolkit.progress(
360354
next(messages), inline_logs=True, lines_to_show=20
361-
) as progress:
362-
with handle_http_errors(progress=progress):
363-
for line in _stream_build_logs(deployment.id):
355+
) as progress, APIClient() as client:
356+
try:
357+
for log in client.stream_build_logs(deployment.id):
364358
time_elapsed = time.monotonic() - started_at
365359

366-
data = json.loads(line)
367-
368-
if "message" in data:
369-
progress.log(Text.from_ansi(data["message"].rstrip()))
360+
if log.type == "message":
361+
progress.log(Text.from_ansi(log.message.rstrip()))
370362

371-
if data.get("type") == "complete":
363+
if log.type == "complete":
372364
progress.log("")
373365
progress.log(
374366
f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
@@ -382,20 +374,28 @@ def _wait_for_deployment(
382374

383375
break
384376

385-
if data.get("type") == "failed":
377+
if log.type == "failed":
386378
progress.log("")
387379
progress.log(
388380
f"😔 Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
389381
)
390382
raise typer.Exit(1)
391383

392384
if time_elapsed > 30:
393-
messages = cycle(LONG_WAIT_MESSAGES) # pragma: no cover
385+
messages = cycle(LONG_WAIT_MESSAGES)
394386

395387
if (time.monotonic() - last_message_changed_at) > 2:
396-
progress.title = next(messages) # pragma: no cover
388+
progress.title = next(messages)
397389

398-
last_message_changed_at = time.monotonic() # pragma: no cover
390+
last_message_changed_at = time.monotonic()
391+
392+
except (BuildLogError, TooManyRetriesError) as e:
393+
logger.error("Build log streaming failed: %s", e)
394+
toolkit.print_line()
395+
toolkit.print(
396+
f"⚠️ Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
397+
)
398+
raise typer.Exit(1) from e
399399

400400

401401
class SignupToWaitingList(BaseModel):
@@ -416,9 +416,7 @@ def _send_waitlist_form(
416416
with toolkit.progress("Sending your request...") as progress:
417417
with APIClient() as client:
418418
with handle_http_errors(progress):
419-
response = client.post(
420-
"/users/waiting-list", json=result.model_dump(mode="json")
421-
)
419+
response = client.post("/users/waiting-list", json=model_dump(result))
422420

423421
response.raise_for_status()
424422

@@ -443,7 +441,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
443441

444442
toolkit.print_line()
445443

446-
result = SignupToWaitingList(email=email)
444+
result = model_validate(SignupToWaitingList, {"email": email})
447445

448446
if toolkit.confirm(
449447
"Do you want to get access faster by giving us more information?",
@@ -467,11 +465,12 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
467465
result = form.run() # type: ignore
468466

469467
try:
470-
result = SignupToWaitingList.model_validate(
468+
result = model_validate(
469+
SignupToWaitingList,
471470
{
472471
"email": email,
473472
**result, # type: ignore
474-
}
473+
},
475474
)
476475
except ValidationError:
477476
toolkit.print(
@@ -499,7 +498,7 @@ def _waitlist_form(toolkit: RichToolkit) -> None:
499498

500499
with contextlib.suppress(Exception):
501500
subprocess.run(
502-
["open", "raycast://confetti?emojis=🐔⚡"],
501+
["open", "-g", "raycast://confetti?emojis=🐔⚡"],
503502
stdout=subprocess.DEVNULL,
504503
stderr=subprocess.DEVNULL,
505504
check=False,

src/fastapi_cloud_cli/commands/env.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from fastapi_cloud_cli.utils.auth import is_logged_in
1212
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
1313
from fastapi_cloud_cli.utils.env import validate_environment_variable_name
14+
from fastapi_cloud_cli.utils.pydantic_compat import model_validate
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -29,7 +30,7 @@ def _get_environment_variables(app_id: str) -> EnvironmentVariableResponse:
2930
response = client.get(f"/apps/{app_id}/environment-variables/")
3031
response.raise_for_status()
3132

32-
return EnvironmentVariableResponse.model_validate(response.json())
33+
return model_validate(EnvironmentVariableResponse, response.json())
3334

3435

3536
def _delete_environment_variable(app_id: str, name: str) -> bool:

src/fastapi_cloud_cli/commands/login.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
write_auth_config,
1717
)
1818
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
1920

2021
logger = logging.getLogger(__name__)
2122

@@ -43,7 +44,7 @@ def _start_device_authorization(
4344

4445
response.raise_for_status()
4546

46-
return AuthorizationData.model_validate(response.json())
47+
return model_validate_json(AuthorizationData, response.text)
4748

4849

4950
def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -> str:
@@ -73,7 +74,7 @@ def _fetch_access_token(client: httpx.Client, device_code: str, interval: int) -
7374

7475
time.sleep(interval)
7576

76-
response_data = TokenResponse.model_validate(response.json())
77+
response_data = model_validate_json(TokenResponse, response.text)
7778

7879
return response_data.access_token
7980

0 commit comments

Comments
 (0)