rdt-cli 0.2.0__py3-none-any.whl
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.
- rdt_cli/__init__.py +3 -0
- rdt_cli/__main__.py +5 -0
- rdt_cli/auth.py +174 -0
- rdt_cli/cli.py +72 -0
- rdt_cli/client.py +356 -0
- rdt_cli/commands/__init__.py +0 -0
- rdt_cli/commands/_common.py +353 -0
- rdt_cli/commands/auth.py +105 -0
- rdt_cli/commands/browse.py +386 -0
- rdt_cli/commands/post.py +183 -0
- rdt_cli/commands/search.py +227 -0
- rdt_cli/commands/social.py +163 -0
- rdt_cli/constants.py +83 -0
- rdt_cli/exceptions.py +69 -0
- rdt_cli/index_cache.py +77 -0
- rdt_cli-0.2.0.dist-info/METADATA +398 -0
- rdt_cli-0.2.0.dist-info/RECORD +19 -0
- rdt_cli-0.2.0.dist-info/WHEEL +4 -0
- rdt_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Common helpers for Reddit CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from ..auth import Credential, get_credential
|
|
18
|
+
from ..client import RedditClient
|
|
19
|
+
from ..exceptions import RedditApiError, SessionExpiredError, error_code_for_exception
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
console = Console(stderr=True)
|
|
24
|
+
error_console = Console(stderr=True)
|
|
25
|
+
_stdout = Console()
|
|
26
|
+
|
|
27
|
+
_SCHEMA_VERSION = "1"
|
|
28
|
+
_OUTPUT_ENV = "OUTPUT"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Shared formatters (DRY — used by browse, search, post) ──────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_score(score: int) -> str:
|
|
35
|
+
"""Format score as human-readable string (e.g., 1.2k)."""
|
|
36
|
+
if score >= 1000:
|
|
37
|
+
return f"{score / 1000:.1f}k"
|
|
38
|
+
return str(score)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_time(ts: float) -> str:
|
|
42
|
+
"""Format Unix timestamp to relative time string."""
|
|
43
|
+
if not ts:
|
|
44
|
+
return "-"
|
|
45
|
+
now = datetime.now(timezone.utc).timestamp()
|
|
46
|
+
diff = now - ts
|
|
47
|
+
if diff < 0:
|
|
48
|
+
return "just now"
|
|
49
|
+
if diff < 60:
|
|
50
|
+
return f"{int(diff)}s ago"
|
|
51
|
+
if diff < 3600:
|
|
52
|
+
return f"{int(diff / 60)}m ago"
|
|
53
|
+
if diff < 86400:
|
|
54
|
+
return f"{int(diff / 3600)}h ago"
|
|
55
|
+
if diff < 604800:
|
|
56
|
+
return f"{int(diff / 86400)}d ago"
|
|
57
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Output format resolution ────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def resolve_output_format(*, as_json: bool, as_yaml: bool) -> str | None:
|
|
64
|
+
"""Resolve explicit flags first, then env override, then TTY default.
|
|
65
|
+
|
|
66
|
+
Returns "json", "yaml", or None (for rich rendering).
|
|
67
|
+
"""
|
|
68
|
+
if as_json and as_yaml:
|
|
69
|
+
raise click.UsageError("Use only one of --json or --yaml.")
|
|
70
|
+
if as_json:
|
|
71
|
+
return "json"
|
|
72
|
+
if as_yaml:
|
|
73
|
+
return "yaml"
|
|
74
|
+
|
|
75
|
+
output_mode = os.getenv(_OUTPUT_ENV, "auto").strip().lower()
|
|
76
|
+
if output_mode == "yaml":
|
|
77
|
+
return "yaml"
|
|
78
|
+
if output_mode == "json":
|
|
79
|
+
return "json"
|
|
80
|
+
if output_mode == "rich":
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if not sys.stdout.isatty():
|
|
84
|
+
return "yaml"
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── Structured output (stable agent envelope) ──────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def success_payload(data: Any) -> dict[str, Any]:
|
|
92
|
+
"""Wrap structured success data in the shared agent schema."""
|
|
93
|
+
return {
|
|
94
|
+
"ok": True,
|
|
95
|
+
"schema_version": _SCHEMA_VERSION,
|
|
96
|
+
"data": data,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def error_payload(code: str, message: str, *, details: Any | None = None) -> dict[str, Any]:
|
|
101
|
+
"""Wrap structured error data in the shared agent schema."""
|
|
102
|
+
error: dict[str, Any] = {
|
|
103
|
+
"code": code,
|
|
104
|
+
"message": message,
|
|
105
|
+
}
|
|
106
|
+
if details is not None:
|
|
107
|
+
error["details"] = details
|
|
108
|
+
return {
|
|
109
|
+
"ok": False,
|
|
110
|
+
"schema_version": _SCHEMA_VERSION,
|
|
111
|
+
"error": error,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def print_json(data: Any) -> None:
|
|
116
|
+
"""Print raw JSON output to stdout."""
|
|
117
|
+
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def print_yaml(data: Any) -> None:
|
|
121
|
+
"""Print raw YAML output to stdout."""
|
|
122
|
+
try:
|
|
123
|
+
import yaml
|
|
124
|
+
|
|
125
|
+
click.echo(yaml.dump(data, allow_unicode=True, default_flow_style=False, sort_keys=False))
|
|
126
|
+
except ImportError:
|
|
127
|
+
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def maybe_print_structured(data: Any, *, as_json: bool, as_yaml: bool) -> bool:
|
|
131
|
+
"""Print structured output (with envelope) when requested or when stdout is non-TTY.
|
|
132
|
+
|
|
133
|
+
Returns True if output was printed, False if rich rendering should be used.
|
|
134
|
+
"""
|
|
135
|
+
fmt = resolve_output_format(as_json=as_json, as_yaml=as_yaml)
|
|
136
|
+
if not fmt:
|
|
137
|
+
return False
|
|
138
|
+
payload = success_payload(data)
|
|
139
|
+
if fmt == "json":
|
|
140
|
+
print_json(payload)
|
|
141
|
+
else:
|
|
142
|
+
print_yaml(payload)
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def emit_error(
|
|
147
|
+
code: str,
|
|
148
|
+
message: str,
|
|
149
|
+
*,
|
|
150
|
+
as_json: bool | None = None,
|
|
151
|
+
as_yaml: bool | None = None,
|
|
152
|
+
details: Any | None = None,
|
|
153
|
+
) -> bool:
|
|
154
|
+
"""Emit a structured error when the active output mode is machine-readable.
|
|
155
|
+
|
|
156
|
+
Returns True if the error was emitted as structured output.
|
|
157
|
+
"""
|
|
158
|
+
if as_json is None or as_yaml is None:
|
|
159
|
+
ctx = click.get_current_context(silent=True)
|
|
160
|
+
params = ctx.params if ctx is not None else {}
|
|
161
|
+
as_json = bool(params.get("as_json", False)) if as_json is None else as_json
|
|
162
|
+
as_yaml = bool(params.get("as_yaml", False)) if as_yaml is None else as_yaml
|
|
163
|
+
|
|
164
|
+
fmt = resolve_output_format(as_json=bool(as_json), as_yaml=bool(as_yaml))
|
|
165
|
+
if fmt is None:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
payload = error_payload(code, message, details=details)
|
|
169
|
+
if fmt == "json":
|
|
170
|
+
print_json(payload)
|
|
171
|
+
else:
|
|
172
|
+
print_yaml(payload)
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ── Auth / Client helpers ───────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def require_auth() -> Credential:
|
|
180
|
+
"""Get credential or exit with error."""
|
|
181
|
+
cred = get_credential()
|
|
182
|
+
if not cred:
|
|
183
|
+
console.print("[yellow]⚠️ Not logged in[/yellow]. Use [bold]rdt login[/bold] to authenticate")
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
return cred
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def optional_auth() -> Credential | None:
|
|
189
|
+
"""Get credential if available, or None (for public endpoints)."""
|
|
190
|
+
return get_credential()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_client(credential: Credential | None = None) -> RedditClient:
|
|
194
|
+
"""Create a RedditClient with optional credential."""
|
|
195
|
+
return RedditClient(credential)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def run_client_action(credential: Credential | None, action: Callable[[RedditClient], T]) -> T:
|
|
199
|
+
"""Run a client action with auto-retry on session expiry."""
|
|
200
|
+
try:
|
|
201
|
+
with get_client(credential) as client:
|
|
202
|
+
return action(client)
|
|
203
|
+
except SessionExpiredError:
|
|
204
|
+
from ..auth import extract_browser_credential
|
|
205
|
+
|
|
206
|
+
fresh = extract_browser_credential()
|
|
207
|
+
if fresh:
|
|
208
|
+
with get_client(fresh) as client:
|
|
209
|
+
return action(client)
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def handle_command(
|
|
214
|
+
credential: Credential | None,
|
|
215
|
+
*,
|
|
216
|
+
action: Callable[[RedditClient], T],
|
|
217
|
+
render: Callable[[T], None] | None = None,
|
|
218
|
+
as_json: bool = False,
|
|
219
|
+
as_yaml: bool = False,
|
|
220
|
+
) -> T | None:
|
|
221
|
+
"""Run a client action with structured output support.
|
|
222
|
+
|
|
223
|
+
- --json → JSON stdout (with envelope)
|
|
224
|
+
- --yaml or non-TTY → YAML (with envelope)
|
|
225
|
+
- Otherwise → rich render
|
|
226
|
+
|
|
227
|
+
On error: emits structured error + exit(1).
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
data = run_client_action(credential, action)
|
|
231
|
+
|
|
232
|
+
if maybe_print_structured(data, as_json=as_json, as_yaml=as_yaml):
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
if render:
|
|
236
|
+
render(data)
|
|
237
|
+
return data
|
|
238
|
+
|
|
239
|
+
except RedditApiError as exc:
|
|
240
|
+
exit_for_error(exc, as_json=as_json, as_yaml=as_yaml)
|
|
241
|
+
return None # unreachable, but for type checker
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def handle_errors(fn: Callable[[], T], *, as_json: bool = False, as_yaml: bool = False) -> T | None:
|
|
245
|
+
"""Run arbitrary command logic and catch RedditApiError."""
|
|
246
|
+
try:
|
|
247
|
+
return fn()
|
|
248
|
+
except RedditApiError as exc:
|
|
249
|
+
exit_for_error(exc, as_json=as_json, as_yaml=as_yaml)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def exit_for_error(
|
|
254
|
+
exc: Exception,
|
|
255
|
+
*,
|
|
256
|
+
as_json: bool = False,
|
|
257
|
+
as_yaml: bool = False,
|
|
258
|
+
prefix: str | None = None,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Emit a structured/non-structured error and terminate the command."""
|
|
261
|
+
message = str(exc)
|
|
262
|
+
if prefix:
|
|
263
|
+
message = f"{prefix}: {message}"
|
|
264
|
+
|
|
265
|
+
code = error_code_for_exception(exc)
|
|
266
|
+
|
|
267
|
+
if emit_error(code, message, as_json=as_json, as_yaml=as_yaml):
|
|
268
|
+
raise SystemExit(1) from None
|
|
269
|
+
|
|
270
|
+
error_console.print(f"[red]❌ [{code}] {message}[/red]")
|
|
271
|
+
raise SystemExit(1) from None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def structured_output_options(command: Callable) -> Callable:
|
|
275
|
+
"""Add --json/--yaml options to a Click command."""
|
|
276
|
+
command = click.option("--yaml", "as_yaml", is_flag=True, help="Output as YAML")(command)
|
|
277
|
+
command = click.option("--json", "as_json", is_flag=True, help="Output as JSON")(command)
|
|
278
|
+
return command
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def listing_options(command: Callable) -> Callable:
|
|
282
|
+
"""Add --json/--yaml/--output/--full-text/--compact options to listing commands."""
|
|
283
|
+
command = click.option(
|
|
284
|
+
"-c", "--compact", is_flag=True,
|
|
285
|
+
help="Compact output (fewer fields, agent-friendly)",
|
|
286
|
+
)(command)
|
|
287
|
+
command = click.option(
|
|
288
|
+
"--full-text", "full_text", is_flag=True,
|
|
289
|
+
help="Show full title/text without truncation",
|
|
290
|
+
)(command)
|
|
291
|
+
command = click.option(
|
|
292
|
+
"-o", "--output", "output_file", default=None,
|
|
293
|
+
help="Save structured output to file (JSON/YAML)",
|
|
294
|
+
)(command)
|
|
295
|
+
command = click.option("--yaml", "as_yaml", is_flag=True, help="Output as YAML")(command)
|
|
296
|
+
command = click.option("--json", "as_json", is_flag=True, help="Output as JSON")(command)
|
|
297
|
+
return command
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def output_or_render(data: Any, *, as_json: bool, as_yaml: bool, render: Callable) -> None:
|
|
301
|
+
"""DRY output routing: JSON / YAML (with envelope) / Rich."""
|
|
302
|
+
if maybe_print_structured(data, as_json=as_json, as_yaml=as_yaml):
|
|
303
|
+
return
|
|
304
|
+
render(data)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def save_output_to_file(data: Any, output_file: str) -> None:
|
|
308
|
+
"""Save structured output to a file (auto-detect JSON/YAML by extension)."""
|
|
309
|
+
payload = success_payload(data)
|
|
310
|
+
ext = output_file.rsplit(".", 1)[-1].lower() if "." in output_file else "json"
|
|
311
|
+
if ext in ("yml", "yaml"):
|
|
312
|
+
try:
|
|
313
|
+
import yaml
|
|
314
|
+
text = yaml.dump(payload, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
|
315
|
+
except ImportError:
|
|
316
|
+
text = json.dumps(payload, indent=2, ensure_ascii=False)
|
|
317
|
+
else:
|
|
318
|
+
text = json.dumps(payload, indent=2, ensure_ascii=False)
|
|
319
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
320
|
+
f.write(text)
|
|
321
|
+
console.print(f"[green]✅ Saved to {output_file}[/green]")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def compact_posts(posts: list[dict]) -> list[dict]:
|
|
325
|
+
"""Strip non-essential fields for agent-friendly compact output."""
|
|
326
|
+
keep = {"id", "name", "title", "subreddit", "author", "score", "num_comments", "permalink", "url", "created_utc"}
|
|
327
|
+
return [{k: v for k, v in p.items() if k in keep} for p in posts]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def write_delay() -> None:
|
|
331
|
+
"""Random delay for write operations (1.5-4s) to mitigate rate limits."""
|
|
332
|
+
import random
|
|
333
|
+
import time
|
|
334
|
+
|
|
335
|
+
delay = random.uniform(1.5, 4.0)
|
|
336
|
+
time.sleep(delay)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def open_url(url: str) -> None:
|
|
340
|
+
"""Open a URL in the default browser."""
|
|
341
|
+
system = platform.system()
|
|
342
|
+
try:
|
|
343
|
+
if system == "Darwin":
|
|
344
|
+
subprocess.run(["open", url], check=True)
|
|
345
|
+
elif system == "Linux":
|
|
346
|
+
subprocess.run(["xdg-open", url], check=True)
|
|
347
|
+
elif system == "Windows":
|
|
348
|
+
subprocess.run(["start", url], check=True, shell=True)
|
|
349
|
+
else:
|
|
350
|
+
click.echo(url)
|
|
351
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
352
|
+
click.echo(url)
|
|
353
|
+
|
rdt_cli/commands/auth.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Auth commands: login, logout, status, whoami."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ._common import (
|
|
8
|
+
console,
|
|
9
|
+
handle_command,
|
|
10
|
+
maybe_print_structured,
|
|
11
|
+
require_auth,
|
|
12
|
+
structured_output_options,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
def login() -> None:
|
|
18
|
+
"""Extract browser cookies for Reddit authentication"""
|
|
19
|
+
from ..auth import extract_browser_credential, get_credential
|
|
20
|
+
|
|
21
|
+
# Check if already logged in
|
|
22
|
+
cred = get_credential()
|
|
23
|
+
if cred:
|
|
24
|
+
console.print("[green]✅ Already authenticated[/green]")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
console.print("[dim]🔍 Searching for Reddit cookies in browsers...[/dim]")
|
|
28
|
+
cred = extract_browser_credential()
|
|
29
|
+
if cred:
|
|
30
|
+
console.print(f"[green]✅ Login successful![/green] ({len(cred.cookies)} cookies extracted)")
|
|
31
|
+
else:
|
|
32
|
+
console.print("[red]❌ No Reddit cookies found.[/red]")
|
|
33
|
+
console.print(" [dim]Please login to reddit.com in your browser first, then retry.[/dim]")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.command()
|
|
37
|
+
def logout() -> None:
|
|
38
|
+
"""Clear saved Reddit cookies"""
|
|
39
|
+
from ..auth import clear_credential
|
|
40
|
+
|
|
41
|
+
clear_credential()
|
|
42
|
+
console.print("[green]✅ Credentials cleared[/green]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command()
|
|
46
|
+
@structured_output_options
|
|
47
|
+
def status(as_json: bool, as_yaml: bool) -> None:
|
|
48
|
+
"""Check authentication status"""
|
|
49
|
+
from ..auth import CREDENTIAL_FILE, get_credential
|
|
50
|
+
|
|
51
|
+
cred = get_credential()
|
|
52
|
+
info = {
|
|
53
|
+
"authenticated": cred is not None,
|
|
54
|
+
"cookie_count": len(cred.cookies) if cred else 0,
|
|
55
|
+
"credential_file": str(CREDENTIAL_FILE),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if maybe_print_structured(info, as_json=as_json, as_yaml=as_yaml):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if cred:
|
|
62
|
+
console.print(f"[green]✅ Authenticated[/green] ({len(cred.cookies)} cookies)")
|
|
63
|
+
if "reddit_session" in cred.cookies:
|
|
64
|
+
console.print(" [dim]reddit_session: ✓[/dim]")
|
|
65
|
+
else:
|
|
66
|
+
console.print("[yellow]⚠️ Not authenticated[/yellow]")
|
|
67
|
+
console.print(" [dim]Use 'rdt login' to extract cookies from your browser[/dim]")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@click.command()
|
|
71
|
+
@structured_output_options
|
|
72
|
+
def whoami(as_json: bool, as_yaml: bool) -> None:
|
|
73
|
+
"""Show current user profile (karma, account age)"""
|
|
74
|
+
from rich.panel import Panel
|
|
75
|
+
|
|
76
|
+
from ._common import format_time
|
|
77
|
+
|
|
78
|
+
cred = require_auth()
|
|
79
|
+
|
|
80
|
+
def _render(data: dict) -> None:
|
|
81
|
+
name = data.get("name", "?")
|
|
82
|
+
karma_post = data.get("link_karma", 0)
|
|
83
|
+
karma_comment = data.get("comment_karma", 0)
|
|
84
|
+
total_karma = data.get("total_karma", karma_post + karma_comment)
|
|
85
|
+
created = data.get("created_utc", 0)
|
|
86
|
+
is_gold = "⭐ " if data.get("is_gold") else ""
|
|
87
|
+
is_mod = "🛡️ " if data.get("is_mod") else ""
|
|
88
|
+
|
|
89
|
+
text = (
|
|
90
|
+
f"[bold cyan]u/{name}[/bold cyan] {is_gold}{is_mod}\n"
|
|
91
|
+
f"📊 Total karma: {total_karma:,}\n"
|
|
92
|
+
f" Post: {karma_post:,} · Comment: {karma_comment:,}\n"
|
|
93
|
+
f"📅 Joined: {format_time(created)}\n"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
panel = Panel(text, title="👤 Me", border_style="green")
|
|
97
|
+
console.print(panel)
|
|
98
|
+
|
|
99
|
+
handle_command(
|
|
100
|
+
cred,
|
|
101
|
+
action=lambda c: c.get_user_about(cred.cookies.get("reddit_user", "me")),
|
|
102
|
+
render=_render,
|
|
103
|
+
as_json=as_json,
|
|
104
|
+
as_yaml=as_yaml,
|
|
105
|
+
)
|