Coverage for src/fastapi_cloud_cli/commands/logs.py: 100%

101 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-11-09 00:25 +0000

1import json 1abcdefg

2import logging 1abcdefg

3import re 1abcdefg

4from datetime import datetime 1abcdefg

5from pathlib import Path 1abcdefg

6from typing import Annotated 1abcdefg

7 

8import typer 1abcdefg

9from httpx import HTTPError 1abcdefg

10from pydantic import BaseModel 1abcdefg

11from rich.markup import escape 1abcdefg

12from rich_toolkit import RichToolkit 1abcdefg

13 

14from fastapi_cloud_cli._app import cloud_app 1abcdefg

15from fastapi_cloud_cli.api import ( 1abcdefg

16 APIClient, 

17 AppLogEntry, 

18 StreamLogError, 

19 TooManyRetriesError, 

20 get_http_error_code, 

21 get_http_error_hint, 

22 handle_http_error, 

23) 

24from fastapi_cloud_cli.commands._auth import UserCommand, get_user_command_context 1abcdefg

25from fastapi_cloud_cli.commands.apps._app import apps_app 1abcdefg

26from fastapi_cloud_cli.utils.apps import resolve_app_id_or_fail 1abcdefg

27from fastapi_cloud_cli.utils.cli import FastAPIRichToolkit 1abcdefg

28from fastapi_cloud_cli.utils.errors import ErrorCode 1abcdefg

29from fastapi_cloud_cli.utils.execution import JsonOutputOption 1abcdefg

30 

31logger = logging.getLogger(__name__) 1abcdefg

32 

33 

34LOG_LEVEL_COLORS = { 1abcdefg

35 "debug": "blue", 

36 "info": "cyan", 

37 "warning": "yellow", 

38 "warn": "yellow", 

39 "error": "red", 

40 "critical": "magenta", 

41 "fatal": "magenta", 

42} 

43 

44SINCE_PATTERN = re.compile(r"^\d+[smhd]$") 1abcdefg

45MIN_LOG_TAIL = 1 1abcdefg

46MAX_LOG_TAIL = 1000 1abcdefg

47 

48 

49class AppLogsOutput(BaseModel): 1abcdefg

50 app_id: str 1abcdefg

51 logs: list[AppLogEntry] 1abcdefg

52 

53 

54def _validate_since(value: str) -> str: 2a b @bc d [be f g ]b

55 """Validate the --since parameter format.""" 

56 if not SINCE_PATTERN.match(value): 2Eby xbZbz A B o + jbqbh = cbC Fb6b^b%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b_b'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b`b(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b{b)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b|b*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b}b+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b~b,b?bYb) * bb

57 raise typer.BadParameter( 2^b_b`b{b|b}b~b

58 "Invalid format. Use a number followed by s, m, h, or d (e.g., '5m', '1h', '2d')." 

59 ) 

60 

61 return value 2Eby xbZbz A B o + jbqbh = cbC Fb6b%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,b?bYb) * bb

62 

63 

64def _validate_tail(value: int) -> int: 2a b @bc d [be f g ]b

65 if not MIN_LOG_TAIL <= value <= MAX_LOG_TAIL: 2Eby xbZbz A B o + jbqbh = cbC Fb6bac%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bbc'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bcc(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bdc)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bec*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bfc+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bgc,b?bYb) * bb

66 raise typer.BadParameter( 2acbcccdcecfcgc

67 f"Invalid value. Use a number between {MIN_LOG_TAIL} and {MAX_LOG_TAIL}." 

68 ) 

69 

70 return value 2Eby xbZbz A B o + jbqbh = cbC Fb6b%b-bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'b.bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(b/bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)b:bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*b;bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+b=bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,b?bYb) * bb

71 

72 

73def _get_log_bullet(log: AppLogEntry) -> str: 1abcdefg

74 """Colored indicator rendered in the emoji bullet column. 

75 

76 ▕ draws at the right edge of its cell, centering the bar under the 

77 double-width emojis. 

78 """ 

79 color = LOG_LEVEL_COLORS.get(log.level.lower(), "dim") 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb

80 

81 return f"[{color}]▕[/{color}]" 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb

82 

83 

84def _format_log_line(log: AppLogEntry) -> str: 1abcdefg

85 """Format a log entry for display""" 

86 # Parse the timestamp string to format it consistently 

87 timestamp = datetime.fromisoformat(log.timestamp.replace("Z", "+00:00")) 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb

88 timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb

89 

90 return f"[dim]{timestamp_str}[/dim] {escape(log.message)}" 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb

91 

92 

93def _print_log_line(toolkit: RichToolkit, log: AppLogEntry) -> None: 1abcdefg

94 toolkit.print(_format_log_line(log), emoji=_get_log_bullet(log)) 2y z A B C D E ? F G p H I J K [ L M N O P Q R ^ S T s U V W X ` Y Z 0 1 2 3 4 | 5 6 7 8 9 ! # ~ $ % w ' ( ) * bb

95 

96 

97def _render_app_logs_output(data: AppLogsOutput, toolkit: RichToolkit) -> None: 1abpcdsefgw

98 if not data.logs: 2Eby z A B C FbGbD E HbF G p H I IbJbJ K KbL M N O P LbMbQ R NbS T s U V ObPbW X QbY Z 0 1 2 RbSb3 4 Tb5 6 7 8 9 UbVb! # Wb$ % w ' ( XbYb) *

99 toolkit.print("No logs found for the specified time range.") 2EbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYb

100 return 2EbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYb

101 

102 for log in data.logs: 1yzABCDEFGpHIJKLMNOPQRSTsUVWXYZ0123456789!#$%w'()*

103 _print_log_line(toolkit, log) 1yzABCDEFGpHIJKLMNOPQRSTsUVWXYZ0123456789!#$%w'()*

104 

105 

106def _print_app_log_json(app_id: str, log: AppLogEntry) -> None: 1abcdefg

107 typer.echo( 2xbybzbAbBbCbDb

108 json.dumps( 

109 { 

110 "type": "log", 

111 "app_id": app_id, 

112 **log.model_dump(mode="json"), 

113 }, 

114 separators=(",", ":"), 

115 ) 

116 ) 

117 

118 

119def _render_plain_error( 1abcdefg

120 toolkit: FastAPIRichToolkit, 

121 *, 

122 code: ErrorCode, 

123 message: str, 

124 hint: str, 

125) -> None: 

126 toolkit.print_error(message) 2o + jbqbh cbq , kbrbi dbr - lbsbj ebt . mbtbk fbu / nbubl gbv : obvbm hbx ; pbwbn ib

127 if hint: 2o + jbqbh cbq , kbrbi dbr - lbsbj ebt . mbtbk fbu / nbubl gbv : obvbm hbx ; pbwbn ib

128 toolkit.print_line() 2o qbh q rbi r sbj t tbk u ubl v vbm x wbn

129 toolkit.print_hint(hint) 2o qbh q rbi r sbj t tbk u ubl v vbm x wbn

130 

131 

132def _handle_stream_log_error( 1abcdefg

133 toolkit: FastAPIRichToolkit, 

134 error: StreamLogError, 

135) -> None: 

136 hint: str | None = None 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib

137 

138 if error.status_code == 404: 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib

139 code: ErrorCode = "not_found" 2jbkblbmbnbobpb

140 message = "App not found. Make sure to use the correct account." 2jbkblbmbnbobpb

141 

142 elif isinstance(error.__cause__, HTTPError): 2o + h = cbq , i @ dbr - j ] ebt . k _ fbu / l { gbv : m } hbx ; n abib

143 code = get_http_error_code(error.__cause__) 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab

144 message = handle_http_error(error.__cause__) 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab

145 hint = get_http_error_hint(code) 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab

146 

147 if error.status_code == 400 and hint is None: 2o + h = q , i @ r - j ] t . k _ u / l { v : m } x ; n ab

148 hint = ( 2h = i @ j ] k _ l { m } n ab

149 "Try a shorter time range (e.g. `--since 1d`). " 

150 "Log retention depends on your plan." 

151 ) 

152 

153 else: 

154 code = "api_error" 2cbdbebfbgbhbib

155 message = f"[red]Error:[/] {escape(str(error))}" 2cbdbebfbgbhbib

156 

157 toolkit.fail(code, message, hint=hint, render_output=_render_plain_error) 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib

158 

159 

160def _process_log_stream( 1abcdefg

161 toolkit: FastAPIRichToolkit, 

162 app_id: str, 

163 tail: int, 

164 since: str, 

165 follow: bool, 

166) -> None: 

167 """Stream app logs and print them to the console.""" 

168 logs: list[AppLogEntry] = [] 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

169 

170 try: 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

171 with APIClient() as client: 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

172 for log in client.stream_app_logs( 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

173 app_id=app_id, 

174 tail=tail, 

175 since=since, 

176 follow=follow, 

177 ): 

178 if follow: 2y xbZbz A B C D E ? F yb0bG p H I J K [ L zb1bM N O P Q R ^ S Ab2bT s U V W X ` Y Bb3bZ 0 1 2 3 4 | 5 Cb4b6 7 8 9 ! # ~ $ Db5b% w ' ( ) * bb

179 if toolkit.mode == "json": 2xb? yb[ zb^ Ab` Bb| Cb~ Dbbb

180 _print_app_log_json(app_id, log) 2xbybzbAbBbCbDb

181 else: 

182 _print_log_line(toolkit, log) 2? [ ^ ` | ~ bb

183 continue 2xb? yb[ zb^ Ab` Bb| Cb~ Dbbb

184 

185 logs.append(log) 2y Zbz A B C D E F 0bG p H I J K L 1bM N O P Q R S 2bT s U V W X Y 3bZ 0 1 2 3 4 5 4b6 7 8 9 ! # $ 5b% w ' ( ) *

186 

187 if not follow: 2Eby xbZbz A B C Fb6bGbD E ? HbF yb0bG p H I Ib7bJbJ K [ KbL zb1bM N O P Lb8bMbQ R ^ NbS Ab2bT s U V Ob9bPbW X ` QbY Bb3bZ 0 1 2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 9 Ub#bVb! # ~ Wb$ Db5b% w ' ( Xb$bYb) * bb

188 toolkit.success( 2Eby Zbz A B C FbGbD E HbF 0bG p H I IbJbJ K KbL 1bM N O P LbMbQ R NbS 2bT s U V ObPbW X QbY 3bZ 0 1 2 RbSb3 4 Tb5 4b6 7 8 9 UbVb! # Wb$ 5b% w ' ( XbYb) *

189 AppLogsOutput(app_id=app_id, logs=logs), 

190 render_output=_render_app_logs_output, 

191 ) 

192 return 2Eby xbZbz A B C Fb6bGbD E ? HbF yb0bG p H I Ib7bJbJ K [ KbL zb1bM N O P Lb8bMbQ R ^ NbS Ab2bT s U V Ob9bPbW X ` QbY Bb3bZ 0 1 2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 9 Ub#bVb! # ~ Wb$ Db5b% w ' ( Xb$bYb) * bb

193 except KeyboardInterrupt: # pragma: no cover 2o + jbqbh = cbq , kbrbi @ dbr - lbsbj ] ebt . mbtbk _ fbu / nbubl { gbv : obvbm } hbx ; pbwbn abib

194 toolkit.print_line() 

195 

196 return 

197 except StreamLogError as e: 2o + jbqbh = cbq , kbrbi @ dbr - lbsbj ] ebt . mbtbk _ fbu / nbubl { gbv : obvbm } hbx ; pbwbn abib

198 _handle_stream_log_error(toolkit, e) 2o + jbh = cbq , kbi @ dbr - lbj ] ebt . mbk _ fbu / nbl { gbv : obm } hbx ; pbn abib

199 except (TooManyRetriesError, TimeoutError): 2qbrbsbtbubvbwb

200 message = "Lost connection to log stream. Please try again later." 2qbrbsbtbubvbwb

201 toolkit.fail( 2qbrbsbtbubvbwb

202 "network_error", 

203 message, 

204 hint="Please try again later.", 

205 render_output=_render_plain_error, 

206 ) 

207 

208 

209@cloud_app.command(cls=UserCommand) 1abcdefg

210@apps_app.command("logs", cls=UserCommand) 1abcdefg

211def logs( 2a b @bc d [be f g ]b

212 ctx: typer.Context, 

213 path: Annotated[ 

214 Path | None, 

215 typer.Argument( 

216 help=( 

217 "Path to the directory with your app's pyproject.toml " 

218 "(defaults to current directory)" 

219 ) 

220 ), 

221 ] = None, 

222 app_id: Annotated[ 

223 str | None, 

224 typer.Option( 

225 "--app-id", 

226 help="ID of the app whose logs should be fetched.", 

227 ), 

228 ] = None, 

229 tail: int = typer.Option( 

230 100, 

231 "--tail", 

232 "-t", 

233 help=f"Number of log lines to show before streaming (max {MAX_LOG_TAIL}).", 

234 show_default=True, 

235 callback=_validate_tail, 

236 ), 

237 since: str = typer.Option( 

238 "5m", 

239 "--since", 

240 "-s", 

241 help=( 

242 "Show logs since a specific time (e.g., '5m', '1h', '2d'). " 

243 "Limited by your plan's log retention." 

244 ), 

245 show_default=True, 

246 callback=_validate_since, 

247 ), 

248 follow: bool = typer.Option( 

249 True, 

250 "--follow/--no-follow", 

251 "-f", 

252 help="Stream logs in real-time (use --no-follow to fetch and exit).", 

253 ), 

254 json_output: JsonOutputOption = False, 

255) -> None: 

256 """Stream or fetch logs from your deployed app. 

257 

258 Examples: 

259 fastapi cloud logs # Stream logs in real-time 

260 fastapi cloud logs --no-follow # Fetch recent logs and exit 

261 fastapi cloud logs --tail 50 --since 1h # Last 50 logs from the past hour 

262 """ 

263 toolkit = get_user_command_context(ctx).toolkit 2Eby xbZbz A B o + jbqbh = cbC Fb6b%bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,bYb) * bb

264 

265 target_app_id = resolve_app_id_or_fail( 2Eby xbZbz A B o + jbqbh = cbC Fb6b%bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7b'bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8b(bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9b)bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!b*bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#b+bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$b,bYb) * bb

266 toolkit, 

267 app_id=app_id, 

268 path=path, 

269 hint="Pass --app-id or run `fastapi cloud link` to link an app.", 

270 ) 

271 

272 logger.debug("Fetching logs for app ID: %s", target_app_id) 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

273 

274 if follow: 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

275 toolkit.print( 2xb6b? yb7b[ zb8b^ Ab9b` Bb!b| Cb#b~ Db$bbb

276 f"Streaming logs for [bold]{target_app_id}[/bold] (Ctrl+C to exit)...", 

277 emoji="📡", 

278 ) 

279 else: 

280 toolkit.print( 2Eby Zbz A B o + jbqbh = cbC FbGbD E HbF 0bG p H q , kbrbi @ dbI IbJbJ K KbL 1bM N O r - lbsbj ] ebP LbMbQ R NbS 2bT s U t . mbtbk _ fbV ObPbW X QbY 3bZ 0 1 u / nbubl { gb2 RbSb3 4 Tb5 4b6 7 8 v : obvbm } hb9 UbVb! # Wb$ 5b% w ' x ; pbwbn abib( XbYb) *

281 f"Fetching logs for [bold]{target_app_id}[/bold]...", 

282 emoji="📜", 

283 ) 

284 toolkit.print_line() 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

285 

286 _process_log_stream( 2Eby xbZbz A B o + jbqbh = cbC Fb6bGbD E ? HbF yb0bG p H q , kbrbi @ dbI Ib7bJbJ K [ KbL zb1bM N O r - lbsbj ] ebP Lb8bMbQ R ^ NbS Ab2bT s U t . mbtbk _ fbV Ob9bPbW X ` QbY Bb3bZ 0 1 u / nbubl { gb2 Rb!bSb3 4 | Tb5 Cb4b6 7 8 v : obvbm } hb9 Ub#bVb! # ~ Wb$ Db5b% w ' x ; pbwbn abib( Xb$bYb) * bb

287 toolkit=toolkit, 

288 app_id=target_app_id, 

289 tail=tail, 

290 since=since, 

291 follow=follow, 

292 )