Skip to content

Commit 2ecc498

Browse files
committed
Add type for build log message
1 parent e1a6dde commit 2ecc498

File tree

3 files changed

+50
-48
lines changed

3 files changed

+50
-48
lines changed

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from typing_extensions import Annotated
2020

2121
from fastapi_cloud_cli.commands.login import login
22-
from fastapi_cloud_cli.utils.api import APIClient, BuildLogError, BuildLogType
22+
from fastapi_cloud_cli.utils.api import APIClient, BuildLogError
2323
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
2424
from fastapi_cloud_cli.utils.auth import is_logged_in
2525
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
@@ -343,10 +343,10 @@ def _wait_for_deployment(
343343
for log in client.stream_build_logs(deployment.id):
344344
time_elapsed = time.monotonic() - started_at
345345

346-
if log.type == BuildLogType.message and log.message:
346+
if log.type == "message":
347347
progress.log(Text.from_ansi(log.message.rstrip()))
348348

349-
if log.type == BuildLogType.complete:
349+
if log.type == "complete":
350350
progress.log("")
351351
progress.log(
352352
f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
@@ -360,7 +360,7 @@ def _wait_for_deployment(
360360

361361
break
362362

363-
if log.type == BuildLogType.failed:
363+
if log.type == "failed":
364364
progress.log("")
365365
progress.log(
366366
f"😔 Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"

src/fastapi_cloud_cli/utils/api.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import time
33
from contextlib import contextmanager
44
from datetime import timedelta
5-
from enum import Enum
6-
from typing import ContextManager, Generator, Optional
5+
from typing import ContextManager, Generator, Literal, Optional, Union
76

87
import httpx
9-
from pydantic import BaseModel, ValidationError
8+
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
9+
from typing_extensions import Annotated
1010

1111
from fastapi_cloud_cli import __version__
1212
from fastapi_cloud_cli.config import Settings
@@ -18,23 +18,27 @@
1818
BUILD_LOG_TIMEOUT = timedelta(minutes=5)
1919

2020

21-
class BuildLogError(Exception): ...
21+
class BuildLogError(Exception):
22+
pass
2223

2324

24-
class BuildLogType(str, Enum):
25-
message = "message"
26-
complete = "complete"
27-
failed = "failed"
28-
timeout = "timeout" # Request closed, reconnect to continue
29-
heartbeat = "heartbeat" # Keepalive signal when no new logs
25+
class BuildLogLineGeneric(BaseModel):
26+
type: Literal["complete", "failed", "timeout", "heartbeat"]
27+
id: Optional[str] = None
3028

3129

32-
class BuildLogLine(BaseModel):
33-
type: BuildLogType
34-
message: Optional[str] = None
30+
class BuildLogLineMessage(BaseModel):
31+
type: Literal["message"] = "message"
32+
message: str
3533
id: Optional[str] = None
3634

3735

36+
BuildLogLine = Union[BuildLogLineMessage, BuildLogLineGeneric]
37+
BuildLogAdapter = TypeAdapter[BuildLogLine](
38+
Annotated[BuildLogLine, Field(discriminator="type")]
39+
)
40+
41+
3842
@contextmanager
3943
def attempt(attempt_number: int) -> Generator[None, None, None]:
4044
def _backoff() -> None:
@@ -132,18 +136,15 @@ def stream_build_logs(
132136
if log_line.id:
133137
last_id = log_line.id
134138

135-
if log_line.type == BuildLogType.message:
139+
if log_line.type == "message":
136140
yield log_line
137141

138-
if log_line.type in (
139-
BuildLogType.complete,
140-
BuildLogType.failed,
141-
):
142+
if log_line.type in ("complete", "failed"):
142143
yield log_line
143144

144145
return
145146

146-
if log_line.type == BuildLogType.timeout:
147+
if log_line.type == "timeout":
147148
logger.debug("Received timeout; reconnecting")
148149
break # Breaks for loop to reconnect
149150

@@ -160,7 +161,7 @@ def stream_build_logs(
160161

161162
def _parse_log_line(self, line: str) -> Optional[BuildLogLine]:
162163
try:
163-
return BuildLogLine.model_validate_json(line)
164+
return BuildLogAdapter.validate_json(line)
164165
except ValidationError as e:
165166
logger.debug("Skipping malformed log: %s (error: %s)", line[:100], e)
166167
return None

tests/test_api_client.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
BUILD_LOG_MAX_RETRIES,
1313
APIClient,
1414
BuildLogError,
15-
BuildLogType,
15+
BuildLogLineMessage,
1616
)
1717
from tests.utils import build_logs_response
1818

@@ -59,13 +59,13 @@ def test_stream_build_logs_successful(
5959

6060
assert len(logs) == 3
6161

62-
assert logs[0].type == BuildLogType.message
62+
assert logs[0].type == "message"
6363
assert logs[0].message == "Building..."
6464

65-
assert logs[1].type == BuildLogType.message
65+
assert logs[1].type == "message"
6666
assert logs[1].message == "Done!"
6767

68-
assert logs[2].type == BuildLogType.complete
68+
assert logs[2].type == "complete"
6969

7070

7171
@api_mock
@@ -85,16 +85,16 @@ def test_stream_build_logs_failed(
8585
logs = list(client.stream_build_logs(deployment_id))
8686

8787
assert len(logs) == 2
88-
assert logs[0].type == BuildLogType.message
89-
assert logs[1].type == BuildLogType.failed
88+
assert logs[0].type == "message"
89+
assert logs[1].type == "failed"
9090

9191

92-
@pytest.mark.parametrize("terminal_type", [BuildLogType.complete, BuildLogType.failed])
92+
@pytest.mark.parametrize("terminal_type", ["complete", "failed"])
9393
@api_mock
9494
def test_stream_build_logs_stop_after_terminal_state(
9595
logs_route: respx.Route,
9696
client: APIClient,
97-
terminal_type: BuildLogType,
97+
terminal_type: str,
9898
deployment_id: str,
9999
) -> None:
100100
logs_route.mock(
@@ -111,7 +111,7 @@ def test_stream_build_logs_stop_after_terminal_state(
111111
logs = list(client.stream_build_logs(deployment_id))
112112

113113
assert len(logs) == 2
114-
assert logs[0].type == BuildLogType.message
114+
assert logs[0].type == "message"
115115
assert logs[1].type == terminal_type
116116

117117

@@ -125,7 +125,7 @@ def test_stream_build_logs_internal_messages_are_skipped(
125125
return_value=Response(
126126
200,
127127
content=build_logs_response(
128-
{"type": BuildLogType.heartbeat, "id": "1"},
128+
{"type": "heartbeat", "id": "1"},
129129
{"type": "message", "message": "Continuing...", "id": "2"},
130130
{"type": "complete", "id": "3"},
131131
),
@@ -135,8 +135,8 @@ def test_stream_build_logs_internal_messages_are_skipped(
135135
logs = list(client.stream_build_logs(deployment_id))
136136

137137
assert len(logs) == 2
138-
assert logs[0].type == BuildLogType.message
139-
assert logs[1].type == BuildLogType.complete
138+
assert logs[0].type == "message"
139+
assert logs[1].type == "complete"
140140

141141

142142
@api_mock
@@ -156,8 +156,8 @@ def test_stream_build_logs_malformed_json_is_skipped(
156156
logs = list(client.stream_build_logs(deployment_id))
157157

158158
assert len(logs) == 2
159-
assert logs[0].type == BuildLogType.message
160-
assert logs[1].type == BuildLogType.complete
159+
assert logs[0].type == "message"
160+
assert logs[1].type == "complete"
161161

162162

163163
@api_mock
@@ -179,8 +179,8 @@ def test_stream_build_logs_unknown_log_type_is_skipped(
179179

180180
# Unknown type should be filtered out
181181
assert len(logs) == 2
182-
assert logs[0].type == BuildLogType.message
183-
assert logs[1].type == BuildLogType.complete
182+
assert logs[0].type == "message"
183+
assert logs[1].type == "complete"
184184

185185

186186
@pytest.mark.parametrize(
@@ -211,6 +211,7 @@ def test_stream_build_logs_network_error_retry(
211211
logs = list(client.stream_build_logs(deployment_id))
212212

213213
assert len(logs) == 2
214+
assert logs[0].type == "message"
214215
assert logs[0].message == "Success after retry"
215216

216217

@@ -232,7 +233,7 @@ def test_stream_build_logs_server_error_retry(
232233
logs = list(client.stream_build_logs(deployment_id))
233234

234235
assert len(logs) == 1
235-
assert logs[0].type == BuildLogType.complete
236+
assert logs[0].type == "complete"
236237

237238

238239
@api_mock
@@ -277,8 +278,8 @@ def test_stream_build_logs_empty_lines_are_skipped(
277278
logs = list(client.stream_build_logs(deployment_id))
278279

279280
assert len(logs) == 2
280-
assert logs[0].type == BuildLogType.message
281-
assert logs[1].type == BuildLogType.complete
281+
assert logs[0].type == "message"
282+
assert logs[1].type == "complete"
282283

283284

284285
@respx.mock(base_url=settings.base_api_url)
@@ -318,11 +319,11 @@ def test_stream_build_logs_continue_after_timeout(
318319
logs = client.stream_build_logs(deployment_id)
319320

320321
with patch("time.sleep"):
321-
assert next(logs).message == "message 1"
322-
assert next(logs).message == "message 2"
323-
assert next(logs).message == "message 3"
324-
assert next(logs).message == "message 4"
325-
assert next(logs).type == BuildLogType.complete
322+
assert next(logs) == BuildLogLineMessage(message="message 1", id="1")
323+
assert next(logs) == BuildLogLineMessage(message="message 2", id="2")
324+
assert next(logs) == BuildLogLineMessage(message="message 3", id="3")
325+
assert next(logs) == BuildLogLineMessage(message="message 4", id="4")
326+
assert next(logs).type == "complete"
326327

327328

328329
@api_mock

0 commit comments

Comments
 (0)