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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # KiteCLI - Kite Connect CLI
cli/api_client.py ADDED
@@ -0,0 +1,322 @@
1
+ """
2
+ Local KiteCLI client.
3
+
4
+ Wraps KiteAccountManager directly — no HTTP server needed.
5
+ Public API is identical to the old HTTP-based KCLIClient so that
6
+ cli/main.py and cli/live_session.py require zero changes.
7
+ """
8
+
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ from cli.kite_manager import KiteAccountManager
11
+
12
+
13
+ class KCLIClientError(Exception):
14
+ """Raised when a Kite API call fails."""
15
+
16
+
17
+ # Module-level singleton so session state persists across calls within the process.
18
+ _manager = KiteAccountManager()
19
+
20
+
21
+ class KCLIClient:
22
+ """Local client that delegates directly to KiteAccountManager.
23
+
24
+ Args:
25
+ accounts: List of account dicts from config (name, api_key, api_secret,
26
+ user_id, password, totp_secret, proxy). Accounts are
27
+ initialised eagerly on construction so that session tokens are
28
+ restored before the first command runs.
29
+ """
30
+
31
+ def __init__(self, accounts: list[dict]) -> None:
32
+ self._accounts = accounts
33
+ self._api_keys = [a.get("api_key", "") for a in accounts]
34
+ # Eagerly init all accounts (restores saved sessions if available)
35
+ for acct in accounts:
36
+ _manager.init_account(
37
+ api_key=acct.get("api_key", ""),
38
+ api_secret=acct.get("api_secret", ""),
39
+ name=acct.get("name", ""),
40
+ proxy=acct.get("proxy"),
41
+ )
42
+
43
+ # ── compatibility helpers ──────────────────────────────────────
44
+
45
+ def health_check(self) -> bool:
46
+ """Always True — no server to ping in local mode."""
47
+ return True
48
+
49
+ # ── public API (mirrors old KCLIClient exactly, but runs in parallel) ──
50
+
51
+ def init_accounts(self, accounts: list[dict]) -> dict:
52
+ """Initialise (or re-initialise) accounts and attempt auto-login in parallel."""
53
+ def init_one(acct):
54
+ api_key = acct.get("api_key", "")
55
+ name = acct.get("name", api_key)
56
+
57
+ login_url = _manager.init_account(
58
+ api_key=api_key,
59
+ api_secret=acct.get("api_secret", ""),
60
+ name=name,
61
+ proxy=acct.get("proxy"),
62
+ )
63
+
64
+ if _manager.is_authenticated(api_key):
65
+ return {
66
+ "name": name,
67
+ "api_key": api_key,
68
+ "login_url": login_url,
69
+ "auto_logged_in": True,
70
+ "message": "Session restored from saved token",
71
+ }
72
+
73
+ user_id = acct.get("user_id")
74
+ password = acct.get("password")
75
+ totp_secret = acct.get("totp_secret")
76
+ if user_id and password and totp_secret:
77
+ success = _manager.auto_login(
78
+ api_key=api_key,
79
+ user_id=user_id,
80
+ password=password,
81
+ totp_secret=totp_secret,
82
+ )
83
+ if success:
84
+ return {
85
+ "name": name,
86
+ "api_key": api_key,
87
+ "login_url": login_url,
88
+ "auto_logged_in": True,
89
+ "message": "Auto-login successful",
90
+ }
91
+ else:
92
+ return {
93
+ "name": name,
94
+ "api_key": api_key,
95
+ "login_url": login_url,
96
+ "auto_logged_in": False,
97
+ "message": "Auto-login failed. Use manual login URL.",
98
+ }
99
+ else:
100
+ return {
101
+ "name": name,
102
+ "api_key": api_key,
103
+ "login_url": login_url,
104
+ "auto_logged_in": False,
105
+ "message": "Credentials incomplete — manual login required.",
106
+ }
107
+
108
+ with ThreadPoolExecutor(max_workers=max(1, len(accounts))) as executor:
109
+ results = list(executor.map(init_one, accounts))
110
+
111
+ return {"accounts": results}
112
+
113
+ def complete_callback(self, api_key: str, request_token: str) -> dict:
114
+ """Complete Kite OAuth login with a request token."""
115
+ try:
116
+ success = _manager.complete_login(api_key, request_token.strip())
117
+ if success:
118
+ return {"status": "success", "message": "Login successful"}
119
+ return {"status": "error", "message": "Login failed — check request_token"}
120
+ except Exception as exc:
121
+ raise KCLIClientError(str(exc)) from exc
122
+
123
+ def get_positions(self, api_keys: list[str]) -> dict:
124
+ """Fetch open positions for the given accounts in parallel."""
125
+ keys = api_keys or _manager.get_all_api_keys()
126
+
127
+ def fetch_one(api_key):
128
+ info = _manager.get_account_info(api_key)
129
+ if not info.get("authenticated"):
130
+ return {
131
+ "name": info.get("name", api_key),
132
+ "api_key": api_key,
133
+ "positions": [],
134
+ "total_pnl": 0.0,
135
+ "status": "unauthenticated",
136
+ }
137
+ try:
138
+ positions = _manager.get_positions(api_key)
139
+ total_pnl = sum(p.get("pnl", 0.0) for p in positions)
140
+ return {
141
+ "name": info.get("name", api_key),
142
+ "api_key": api_key,
143
+ "positions": positions,
144
+ "total_pnl": total_pnl,
145
+ "status": "success",
146
+ }
147
+ except Exception as exc:
148
+ return {
149
+ "name": info.get("name", api_key),
150
+ "api_key": api_key,
151
+ "positions": [],
152
+ "total_pnl": 0.0,
153
+ "status": f"error: {exc}",
154
+ }
155
+
156
+ with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
157
+ results = list(executor.map(fetch_one, keys))
158
+
159
+ return {"accounts": results}
160
+
161
+ def get_status(self) -> dict:
162
+ """Get authentication status for all accounts."""
163
+ accounts = [
164
+ _manager.get_account_info(api_key)
165
+ for api_key in _manager.get_all_api_keys()
166
+ ]
167
+ return {"accounts": accounts}
168
+
169
+ def place_order(
170
+ self,
171
+ api_keys: list[str],
172
+ tradingsymbol: str,
173
+ exchange: str,
174
+ transaction_type: str,
175
+ quantity: int,
176
+ order_type: str,
177
+ price: float | None = None,
178
+ trigger_price: float | None = None,
179
+ product: str = "NRML",
180
+ ) -> dict:
181
+ """Place an order across specified accounts in parallel."""
182
+ keys = api_keys or _manager.get_all_api_keys()
183
+
184
+ def place_one(api_key):
185
+ info = _manager.get_account_info(api_key)
186
+ if not info.get("authenticated"):
187
+ return {
188
+ "name": info.get("name", api_key),
189
+ "api_key": api_key,
190
+ "status": "error",
191
+ "order_id": None,
192
+ "message": "Account not authenticated",
193
+ }
194
+ try:
195
+ order_id = _manager.place_order(
196
+ api_key=api_key,
197
+ tradingsymbol=tradingsymbol,
198
+ exchange=exchange,
199
+ transaction_type=transaction_type,
200
+ quantity=quantity,
201
+ order_type=order_type,
202
+ price=price,
203
+ trigger_price=trigger_price,
204
+ product=product,
205
+ )
206
+ return {
207
+ "name": info.get("name", api_key),
208
+ "api_key": api_key,
209
+ "status": "success",
210
+ "order_id": str(order_id),
211
+ "message": f"Order placed: {order_id}",
212
+ }
213
+ except Exception as exc:
214
+ return {
215
+ "name": info.get("name", api_key),
216
+ "api_key": api_key,
217
+ "status": "error",
218
+ "order_id": None,
219
+ "message": str(exc),
220
+ }
221
+
222
+ with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
223
+ results = list(executor.map(place_one, keys))
224
+
225
+ return {"results": results}
226
+
227
+ def exit_positions(
228
+ self,
229
+ api_keys: list[str],
230
+ tradingsymbol: str | None = None,
231
+ ) -> dict:
232
+ """Exit positions across specified accounts in parallel."""
233
+ keys = api_keys or _manager.get_all_api_keys()
234
+
235
+ def exit_one(api_key):
236
+ info = _manager.get_account_info(api_key)
237
+ if not info.get("authenticated"):
238
+ return {
239
+ "name": info.get("name", api_key),
240
+ "api_key": api_key,
241
+ "status": "error",
242
+ "message": "Account not authenticated",
243
+ "orders_placed": [],
244
+ }
245
+ try:
246
+ orders = _manager.exit_positions(api_key, tradingsymbol)
247
+ return {
248
+ "name": info.get("name", api_key),
249
+ "api_key": api_key,
250
+ "status": "success",
251
+ "message": f"Exited {len(orders)} position(s)",
252
+ "orders_placed": orders,
253
+ }
254
+ except Exception as exc:
255
+ return {
256
+ "name": info.get("name", api_key),
257
+ "api_key": api_key,
258
+ "status": "error",
259
+ "message": str(exc),
260
+ "orders_placed": [],
261
+ }
262
+
263
+ with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
264
+ results = list(executor.map(exit_one, keys))
265
+
266
+ return {"results": results}
267
+
268
+ def get_option_chain(
269
+ self,
270
+ api_key: str,
271
+ underlying: str,
272
+ expiry_week: int = 0,
273
+ expiry_date: str | None = None,
274
+ ) -> dict:
275
+ """Fetch option chain for a specific underlying and expiry."""
276
+ try:
277
+ return _manager.get_option_chain(
278
+ api_key=api_key,
279
+ underlying=underlying,
280
+ expiry_week=expiry_week,
281
+ expiry_date=expiry_date,
282
+ )
283
+ except Exception as exc:
284
+ raise KCLIClientError(str(exc)) from exc
285
+
286
+ def get_orders(self, api_keys: list[str]) -> dict:
287
+ """Fetch today's order book for specified accounts in parallel."""
288
+ keys = api_keys or _manager.get_all_api_keys()
289
+
290
+ def fetch_one(api_key):
291
+ info = _manager.get_account_info(api_key)
292
+ if not info.get("authenticated"):
293
+ return {
294
+ "name": info.get("name", api_key),
295
+ "api_key": api_key,
296
+ "orders": [],
297
+ "status": "unauthenticated",
298
+ }
299
+ try:
300
+ orders = _manager.get_orders(api_key)
301
+ return {
302
+ "name": info.get("name", api_key),
303
+ "api_key": api_key,
304
+ "orders": orders,
305
+ "status": "success",
306
+ }
307
+ except Exception as exc:
308
+ return {
309
+ "name": info.get("name", api_key),
310
+ "api_key": api_key,
311
+ "orders": [],
312
+ "status": f"error: {exc}",
313
+ }
314
+
315
+ with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
316
+ results = list(executor.map(fetch_one, keys))
317
+
318
+ return {"accounts": results}
319
+
320
+ def get_market_indices(self) -> dict:
321
+ """Fetch live Nifty, Sensex, and India VIX."""
322
+ return _manager.get_market_indices()
cli/config.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ Configuration management for KiteCLI.
3
+
4
+ Handles reading, writing, and creating default configuration files
5
+ stored at ~/.kcli/config.yaml.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import yaml
12
+
13
+ CONFIG_DIR = Path.home() / ".kcli"
14
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
+
16
+ DEFAULT_CONFIG = {
17
+ "accounts": [
18
+ {
19
+ "name": "Account 1",
20
+ "api_key": "your_api_key",
21
+ "api_secret": "your_api_secret",
22
+ "user_id": "your_zerodha_user_id",
23
+ "password": "your_zerodha_password",
24
+ "totp_secret": "your_totp_secret",
25
+ "proxy": "http://user:pass@host:port",
26
+ },
27
+ ],
28
+ }
29
+
30
+
31
+ def load_config() -> Optional[dict]:
32
+ """Load configuration from ~/.kcli/config.yaml.
33
+
34
+ Returns:
35
+ dict with configuration values, or None if the config file
36
+ does not exist.
37
+ """
38
+ if not CONFIG_FILE.exists():
39
+ return None
40
+
41
+ with open(CONFIG_FILE, "r") as f:
42
+ config = yaml.safe_load(f)
43
+
44
+ # Automatically migrate/remove legacy server config if present
45
+ if config and "server" in config:
46
+ del config["server"]
47
+ save_config(config)
48
+
49
+ return config
50
+
51
+
52
+ def save_config(config: dict) -> None:
53
+ """Write configuration to ~/.kcli/config.yaml.
54
+
55
+ Creates the config directory if it does not already exist.
56
+
57
+ Args:
58
+ config: Configuration dictionary to persist.
59
+ """
60
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
61
+ with open(CONFIG_FILE, "w") as f:
62
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
63
+
64
+
65
+ def create_default_config() -> dict:
66
+ """Create and save a default template configuration.
67
+
68
+ The template contains placeholder values that the user should
69
+ replace with their own credentials.
70
+
71
+ Returns:
72
+ The default configuration dictionary that was saved.
73
+ """
74
+ save_config(DEFAULT_CONFIG)
75
+ return DEFAULT_CONFIG