kitecli 0.1.0b1__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.
cli/display.py ADDED
@@ -0,0 +1,367 @@
1
+ """
2
+ Rich-based display helpers for KiteCLI.
3
+
4
+ All terminal output—banners, tables, panels, status messages—goes
5
+ through the functions in this module so the CLI has a consistent,
6
+ polished look.
7
+ """
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ console = Console()
15
+
16
+ # ── Banner ─────────────────────────────────────────────────────────
17
+
18
+ BANNER = r"""
19
+ ╦╔═╔═╗╦ ╦
20
+ ╠╩╗║ ║ ║
21
+ ╩ ╩╚═╝╩═╝╩
22
+ Kite Connect CLI
23
+ """
24
+
25
+
26
+ def display_banner() -> None:
27
+ """Print the KiteCLI banner in bold blue/cyan."""
28
+ console.print(Text(BANNER, style="bold #58a6ff"))
29
+
30
+
31
+ # ── Positions ──────────────────────────────────────────────────────
32
+
33
+ def _format_currency(value: float) -> str:
34
+ """Format a number as ₹ with commas (Indian locale style)."""
35
+ sign = "-" if value < 0 else ""
36
+ abs_val = abs(value)
37
+ # Split into integer and decimal parts
38
+ int_part = int(abs_val)
39
+ dec_part = f"{abs_val - int_part:.2f}"[1:] # ".xx"
40
+
41
+ # Indian grouping: last 3 digits, then groups of 2
42
+ s = str(int_part)
43
+ if len(s) > 3:
44
+ last3 = s[-3:]
45
+ rest = s[:-3]
46
+ groups = []
47
+ while rest:
48
+ groups.append(rest[-2:])
49
+ rest = rest[:-2]
50
+ groups.reverse()
51
+ formatted = ",".join(groups) + "," + last3
52
+ else:
53
+ formatted = s
54
+
55
+ return f"{sign}₹{formatted}{dec_part}"
56
+
57
+
58
+ def _pnl_style(value: float) -> str:
59
+ """Return soft green/red/dim colors for positive/negative P&L."""
60
+ if value > 0:
61
+ return "#3fb950"
62
+ elif value < 0:
63
+ return "#f85149"
64
+ return "#8b949e"
65
+
66
+
67
+ def _format_pnl_pct(value: float) -> str:
68
+ """Format P&L percentage with + / - prefix."""
69
+ prefix = "+" if value > 0 else ""
70
+ return f"{prefix}{value:.2f}%"
71
+
72
+
73
+ def display_positions(accounts_data: list[dict]) -> None:
74
+ """Render positions tables for every account.
75
+
76
+ Args:
77
+ accounts_data: List of dicts, each with keys ``name``,
78
+ ``total_pnl``, and ``positions`` (list of position dicts).
79
+ """
80
+ grand_total_pnl = 0.0
81
+
82
+ for account in accounts_data:
83
+ name = account.get("name", "Unknown")
84
+ total_pnl = float(account.get("total_pnl", 0))
85
+ positions = account.get("positions", [])
86
+ grand_total_pnl += total_pnl
87
+
88
+ pnl_color = _pnl_style(total_pnl)
89
+ header_text = Text.assemble(
90
+ (f" {name} ", "bold #e6edf3"),
91
+ (" │ ", "#8b949e"),
92
+ ("P&L: ", "bold"),
93
+ (f"{_format_currency(total_pnl)}", f"bold {pnl_color}"),
94
+ )
95
+
96
+ if not positions:
97
+ panel = Panel(
98
+ Text(" No open positions", style="#8b949e italic"),
99
+ title=header_text,
100
+ border_style=pnl_color,
101
+ padding=(1, 2),
102
+ )
103
+ console.print(panel)
104
+ console.print()
105
+ continue
106
+
107
+ table = Table(
108
+ show_header=True,
109
+ header_style="bold #58a6ff",
110
+ border_style="#30363d",
111
+ row_styles=["", "dim"],
112
+ pad_edge=True,
113
+ expand=True,
114
+ )
115
+ table.add_column("Symbol", style="bold #e6edf3", no_wrap=True)
116
+ table.add_column("Qty", justify="right")
117
+ table.add_column("Avg Price", justify="right")
118
+ table.add_column("LTP", justify="right")
119
+ table.add_column("P&L", justify="right")
120
+ table.add_column("P&L %", justify="right")
121
+
122
+ for pos in positions:
123
+ pnl = float(pos.get("pnl", 0))
124
+ pnl_pct = float(pos.get("pnl_pct", 0))
125
+ style = _pnl_style(pnl)
126
+
127
+ table.add_row(
128
+ str(pos.get("tradingsymbol", "")),
129
+ str(pos.get("quantity", 0)),
130
+ _format_currency(float(pos.get("average_price", 0))),
131
+ _format_currency(float(pos.get("last_price", 0))),
132
+ Text(_format_currency(pnl), style=style),
133
+ Text(_format_pnl_pct(pnl_pct), style=style),
134
+ )
135
+
136
+ panel = Panel(
137
+ table,
138
+ title=header_text,
139
+ border_style=pnl_color,
140
+ padding=(0, 1),
141
+ )
142
+ console.print(panel)
143
+ console.print()
144
+
145
+ # Grand total summary
146
+ gt_style = _pnl_style(grand_total_pnl)
147
+ summary = Text.assemble(
148
+ (" Grand Total P&L ", "bold"),
149
+ (f"{_format_currency(grand_total_pnl)}", f"bold {gt_style}"),
150
+ )
151
+ console.print(
152
+ Panel(
153
+ summary,
154
+ border_style=gt_style,
155
+ padding=(1, 2),
156
+ )
157
+ )
158
+
159
+
160
+ def render_positions_to_string(accounts_data: list[dict], width: int = 80, show_indices: bool = False) -> str:
161
+ """Render positions to a string with ANSI color codes.
162
+
163
+ Identical to display_positions, but returns the string instead of printing.
164
+ """
165
+ from io import StringIO
166
+ from rich.console import Console
167
+
168
+ # Create an in-memory console capturing output
169
+ capture_console = Console(
170
+ file=StringIO(),
171
+ force_terminal=True,
172
+ color_system="truecolor",
173
+ width=width,
174
+ )
175
+
176
+ grand_total_pnl = 0.0
177
+ pos_idx = 1
178
+
179
+ for account in accounts_data:
180
+ name = account.get("name", "Unknown")
181
+ total_pnl = float(account.get("total_pnl", 0))
182
+ positions = account.get("positions", [])
183
+ grand_total_pnl += total_pnl
184
+
185
+ pnl_color = _pnl_style(total_pnl)
186
+ header_text = Text.assemble(
187
+ (f" {name} ", "bold #e6edf3"),
188
+ (" │ ", "#8b949e"),
189
+ ("P&L: ", "bold"),
190
+ (f"{_format_currency(total_pnl)}", f"bold {pnl_color}"),
191
+ )
192
+
193
+ if not positions:
194
+ panel = Panel(
195
+ Text(" No open positions", style="#8b949e italic"),
196
+ title=header_text,
197
+ border_style=pnl_color,
198
+ padding=(1, 2),
199
+ )
200
+ capture_console.print(panel)
201
+ capture_console.print()
202
+ continue
203
+
204
+ table = Table(
205
+ show_header=True,
206
+ header_style="bold #58a6ff",
207
+ border_style="#30363d",
208
+ row_styles=["", "dim"],
209
+ pad_edge=True,
210
+ expand=True,
211
+ )
212
+ table.add_column("Symbol", style="bold #e6edf3", no_wrap=True)
213
+ table.add_column("Qty", justify="right")
214
+ table.add_column("Avg Price", justify="right")
215
+ table.add_column("LTP", justify="right")
216
+ table.add_column("P&L", justify="right")
217
+ table.add_column("P&L %", justify="right")
218
+
219
+ for pos in positions:
220
+ pnl = float(pos.get("pnl", 0))
221
+ pnl_pct = float(pos.get("pnl_pct", 0))
222
+ style = _pnl_style(pnl)
223
+
224
+ symbol = str(pos.get("tradingsymbol", ""))
225
+ if show_indices:
226
+ symbol = f"[{pos_idx}] {symbol}"
227
+ pos_idx += 1
228
+
229
+ table.add_row(
230
+ symbol,
231
+ str(pos.get("quantity", 0)),
232
+ _format_currency(float(pos.get("average_price", 0))),
233
+ _format_currency(float(pos.get("last_price", 0))),
234
+ Text(_format_currency(pnl), style=style),
235
+ Text(_format_pnl_pct(pnl_pct), style=style),
236
+ )
237
+
238
+
239
+ panel = Panel(
240
+ table,
241
+ title=header_text,
242
+ border_style=pnl_color,
243
+ padding=(0, 1),
244
+ )
245
+ capture_console.print(panel)
246
+ capture_console.print()
247
+
248
+ # Grand total summary
249
+ gt_style = _pnl_style(grand_total_pnl)
250
+ summary_text = Text.assemble(
251
+ (" Grand Total P&L ", "bold"),
252
+ (f"{_format_currency(grand_total_pnl)}", f"bold {gt_style}"),
253
+ )
254
+ capture_console.print(
255
+ Panel(
256
+ summary_text,
257
+ border_style=gt_style,
258
+ padding=(1, 2),
259
+ )
260
+ )
261
+
262
+ return capture_console.file.getvalue()
263
+
264
+
265
+ # ── Login URLs ─────────────────────────────────────────────────────
266
+
267
+ def display_login_urls(accounts: list[dict]) -> None:
268
+ """Show Kite login URLs for each account.
269
+
270
+ Args:
271
+ accounts: List of dicts with ``name`` and ``login_url`` keys.
272
+ """
273
+ lines = Text()
274
+ for acct in accounts:
275
+ lines.append(" ✓ ", style="bold #3fb950")
276
+ lines.append(f"{acct.get('name', 'Account')}", style="bold #e6edf3")
277
+ lines.append("\n ", style="")
278
+ lines.append(f"{acct.get('login_url', 'N/A')}", style="underline #58a6ff")
279
+ lines.append("\n\n")
280
+
281
+ lines.append(
282
+ " Open each URL in your browser to authorize the account.\n",
283
+ style="#8b949e italic",
284
+ )
285
+
286
+ console.print(
287
+ Panel(
288
+ lines,
289
+ title="[bold #58a6ff]🔗 Login URLs[/bold #58a6ff]",
290
+ border_style="#58a6ff",
291
+ padding=(1, 2),
292
+ )
293
+ )
294
+
295
+
296
+ # ── Status ─────────────────────────────────────────────────────────
297
+
298
+ def display_status(accounts: list[dict]) -> None:
299
+ """Show per-account authentication status.
300
+
301
+ Args:
302
+ accounts: List of dicts with ``name`` and ``authenticated`` keys.
303
+ """
304
+ table = Table(
305
+ show_header=True,
306
+ header_style="bold #58a6ff",
307
+ border_style="#30363d",
308
+ pad_edge=True,
309
+ )
310
+ table.add_column("Account", style="bold #e6edf3")
311
+ table.add_column("Status", justify="center")
312
+
313
+ for acct in accounts:
314
+ name = acct.get("name", "Unknown")
315
+ authed = acct.get("authenticated", False)
316
+ if authed:
317
+ status = Text("✓ Authenticated", style="bold #3fb950")
318
+ else:
319
+ status = Text("✗ Not Authenticated", style="bold #f85149")
320
+ table.add_row(name, status)
321
+
322
+ console.print(
323
+ Panel(
324
+ table,
325
+ title="[bold #58a6ff]📊 Account Status[/bold #58a6ff]",
326
+ border_style="#58a6ff",
327
+ padding=(1, 1),
328
+ )
329
+ )
330
+
331
+
332
+ # ── Simple messages ────────────────────────────────────────────────
333
+
334
+ def display_error(message: str) -> None:
335
+ """Print a styled error message."""
336
+ console.print(
337
+ Panel(
338
+ Text(f" {message}", style="bold #f85149"),
339
+ title="[bold #f85149]✗ Error[/bold #f85149]",
340
+ border_style="#f85149",
341
+ padding=(0, 1),
342
+ )
343
+ )
344
+
345
+
346
+ def display_success(message: str) -> None:
347
+ """Print a styled success message."""
348
+ console.print(
349
+ Panel(
350
+ Text(f" {message}", style="bold #3fb950"),
351
+ title="[bold #3fb950]✓ Success[/bold #3fb950]",
352
+ border_style="#3fb950",
353
+ padding=(0, 1),
354
+ )
355
+ )
356
+
357
+
358
+ def display_info(message: str) -> None:
359
+ """Print a styled informational message."""
360
+ console.print(
361
+ Panel(
362
+ Text(f" {message}", style="bold #58a6ff"),
363
+ title="[bold #58a6ff]ℹ Info[/bold #58a6ff]",
364
+ border_style="#58a6ff",
365
+ padding=(0, 1),
366
+ )
367
+ )