kitecli 0.1.0b1__tar.gz

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.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: kitecli
3
+ Version: 0.1.0b1
4
+ Summary: Kite Connect CLI — Multi-account Zerodha trading positions viewer
5
+ Author: KiteCLI Team
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer>=0.9.0
10
+ Requires-Dist: rich>=13.0.0
11
+ Requires-Dist: pyyaml>=6.0.0
12
+ Requires-Dist: prompt_toolkit>=3.0.36
13
+ Requires-Dist: kiteconnect>=5.0.0
14
+ Requires-Dist: pyotp>=2.9.0
15
+ Requires-Dist: requests>=2.31.0
16
+ Requires-Dist: yfinance>=0.2.0
17
+ Provides-Extra: server
18
+ Requires-Dist: fastapi>=0.110.0; extra == "server"
19
+ Requires-Dist: uvicorn[standard]>=0.27.0; extra == "server"
20
+ Requires-Dist: pydantic>=2.0.0; extra == "server"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
24
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
25
+
26
+ # KCLI — Kite Connect CLI
27
+
28
+ A multi-account Zerodha Kite Connect positions viewer with a beautiful terminal interface.
29
+
30
+ ```
31
+ ╦╔═╔═╗╦ ╦
32
+ ╠╩╗║ ║ ║
33
+ ╩ ╩╚═╝╩═╝╩
34
+ Kite Connect CLI
35
+ ```
36
+
37
+ ## Architecture
38
+
39
+ ```
40
+ ┌─────────────────┐ HTTPS ┌──────────────────┐ Kite API ┌─────────────────┐
41
+ │ kcli (CLI) │ ──────────────── │ FastAPI Server │ ──────────────────│ Zerodha Kite │
42
+ │ Your Machine │ │ Google Cloud │ │ Connect API │
43
+ └─────────────────┘ └──────────────────┘ └─────────────────┘
44
+
45
+
46
+ ~/.kcli/config.yaml
47
+ ```
48
+
49
+ - **CLI (`kcli`)**: Runs on your machine. Beautiful color-coded terminal UI.
50
+ - **Server**: Runs on Google Cloud (Cloud Run). Proxies Kite API calls.
51
+ - **Config**: Multi-account config stored locally at `~/.kcli/config.yaml`.
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Deploy the Server
56
+
57
+ ```bash
58
+ # Build and deploy to Cloud Run
59
+ cd server
60
+ gcloud run deploy kcli-server \
61
+ --source . \
62
+ --region asia-south1 \
63
+ --allow-unauthenticated \
64
+ --set-env-vars AUTH_TOKEN=your-secret-token
65
+ ```
66
+
67
+ ### 2. Install the CLI
68
+
69
+ ```bash
70
+ # From the project root
71
+ pip install -e .
72
+ ```
73
+
74
+ ### 3. Initialize Config
75
+
76
+ ```bash
77
+ # Create the default config file
78
+ kcli config --init
79
+
80
+ # Edit the config with your accounts
81
+ # Open ~/.kcli/config.yaml and add your Kite API credentials
82
+ ```
83
+
84
+ **Config file format (`~/.kcli/config.yaml`):**
85
+
86
+ ```yaml
87
+ server:
88
+ url: "https://your-cloud-run-url.run.app"
89
+ auth_token: "your-secret-token"
90
+
91
+ accounts:
92
+ - name: "Trading Account 1"
93
+ api_key: "your_api_key_1"
94
+ api_secret: "your_api_secret_1"
95
+ - name: "Trading Account 2"
96
+ api_key: "your_api_key_2"
97
+ api_secret: "your_api_secret_2"
98
+ ```
99
+
100
+ ### 4. Login to Kite
101
+
102
+ ```bash
103
+ # Initialize and authenticate all accounts
104
+ kcli init
105
+ ```
106
+
107
+ This will:
108
+ 1. Send your account configs to the server
109
+ 2. Display login URLs for each account
110
+ 3. Prompt you to paste the `request_token` after logging in via browser
111
+
112
+ > **Note:** Kite access tokens expire daily (~6 AM IST). You need to run `kcli init` once each trading day.
113
+
114
+ ### 5. View Positions
115
+
116
+ ```bash
117
+ # View positions across all accounts
118
+ kcli positions
119
+ ```
120
+
121
+ ## Commands
122
+
123
+ | Command | Description |
124
+ |---|---|
125
+ | `kcli init` | Authenticate all accounts (daily) |
126
+ | `kcli positions` | View positions across all accounts |
127
+ | `kcli status` | Check authentication status |
128
+ | `kcli config --init` | Create default config file |
129
+ | `kcli config --show` | Show current config (secrets masked) |
130
+ | `kcli config --path` | Print config file path |
131
+
132
+ ## Development
133
+
134
+ ### Run Server Locally
135
+
136
+ ```bash
137
+ cd server
138
+ pip install -r requirements.txt
139
+ AUTH_TOKEN=test-token uvicorn main:app --reload --port 8080
140
+ ```
141
+
142
+ ### Run CLI
143
+
144
+ ```bash
145
+ pip install -e .
146
+ kcli --help
147
+ ```
148
+
149
+ ## Getting Kite API Credentials
150
+
151
+ 1. Go to [Kite Developer Console](https://developers.kite.trade/)
152
+ 2. Create a new app
153
+ 3. Note your **API Key** and **API Secret**
154
+ 4. Set the **Redirect URL** to your server's callback URL
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,133 @@
1
+ # KCLI — Kite Connect CLI
2
+
3
+ A multi-account Zerodha Kite Connect positions viewer with a beautiful terminal interface.
4
+
5
+ ```
6
+ ╦╔═╔═╗╦ ╦
7
+ ╠╩╗║ ║ ║
8
+ ╩ ╩╚═╝╩═╝╩
9
+ Kite Connect CLI
10
+ ```
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ ┌─────────────────┐ HTTPS ┌──────────────────┐ Kite API ┌─────────────────┐
16
+ │ kcli (CLI) │ ──────────────── │ FastAPI Server │ ──────────────────│ Zerodha Kite │
17
+ │ Your Machine │ │ Google Cloud │ │ Connect API │
18
+ └─────────────────┘ └──────────────────┘ └─────────────────┘
19
+
20
+
21
+ ~/.kcli/config.yaml
22
+ ```
23
+
24
+ - **CLI (`kcli`)**: Runs on your machine. Beautiful color-coded terminal UI.
25
+ - **Server**: Runs on Google Cloud (Cloud Run). Proxies Kite API calls.
26
+ - **Config**: Multi-account config stored locally at `~/.kcli/config.yaml`.
27
+
28
+ ## Quick Start
29
+
30
+ ### 1. Deploy the Server
31
+
32
+ ```bash
33
+ # Build and deploy to Cloud Run
34
+ cd server
35
+ gcloud run deploy kcli-server \
36
+ --source . \
37
+ --region asia-south1 \
38
+ --allow-unauthenticated \
39
+ --set-env-vars AUTH_TOKEN=your-secret-token
40
+ ```
41
+
42
+ ### 2. Install the CLI
43
+
44
+ ```bash
45
+ # From the project root
46
+ pip install -e .
47
+ ```
48
+
49
+ ### 3. Initialize Config
50
+
51
+ ```bash
52
+ # Create the default config file
53
+ kcli config --init
54
+
55
+ # Edit the config with your accounts
56
+ # Open ~/.kcli/config.yaml and add your Kite API credentials
57
+ ```
58
+
59
+ **Config file format (`~/.kcli/config.yaml`):**
60
+
61
+ ```yaml
62
+ server:
63
+ url: "https://your-cloud-run-url.run.app"
64
+ auth_token: "your-secret-token"
65
+
66
+ accounts:
67
+ - name: "Trading Account 1"
68
+ api_key: "your_api_key_1"
69
+ api_secret: "your_api_secret_1"
70
+ - name: "Trading Account 2"
71
+ api_key: "your_api_key_2"
72
+ api_secret: "your_api_secret_2"
73
+ ```
74
+
75
+ ### 4. Login to Kite
76
+
77
+ ```bash
78
+ # Initialize and authenticate all accounts
79
+ kcli init
80
+ ```
81
+
82
+ This will:
83
+ 1. Send your account configs to the server
84
+ 2. Display login URLs for each account
85
+ 3. Prompt you to paste the `request_token` after logging in via browser
86
+
87
+ > **Note:** Kite access tokens expire daily (~6 AM IST). You need to run `kcli init` once each trading day.
88
+
89
+ ### 5. View Positions
90
+
91
+ ```bash
92
+ # View positions across all accounts
93
+ kcli positions
94
+ ```
95
+
96
+ ## Commands
97
+
98
+ | Command | Description |
99
+ |---|---|
100
+ | `kcli init` | Authenticate all accounts (daily) |
101
+ | `kcli positions` | View positions across all accounts |
102
+ | `kcli status` | Check authentication status |
103
+ | `kcli config --init` | Create default config file |
104
+ | `kcli config --show` | Show current config (secrets masked) |
105
+ | `kcli config --path` | Print config file path |
106
+
107
+ ## Development
108
+
109
+ ### Run Server Locally
110
+
111
+ ```bash
112
+ cd server
113
+ pip install -r requirements.txt
114
+ AUTH_TOKEN=test-token uvicorn main:app --reload --port 8080
115
+ ```
116
+
117
+ ### Run CLI
118
+
119
+ ```bash
120
+ pip install -e .
121
+ kcli --help
122
+ ```
123
+
124
+ ## Getting Kite API Credentials
125
+
126
+ 1. Go to [Kite Developer Console](https://developers.kite.trade/)
127
+ 2. Create a new app
128
+ 3. Note your **API Key** and **API Secret**
129
+ 4. Set the **Redirect URL** to your server's callback URL
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1 @@
1
+ # KiteCLI - Kite Connect CLI
@@ -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()
@@ -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