pykalshi 0.1.0__py3-none-any.whl → 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.
@@ -0,0 +1,171 @@
1
+ """
2
+ Rate limiter for proactive request throttling.
3
+
4
+ Composable, optional, and easy to remove. Inject into KalshiClient
5
+ to prevent 429s rather than just retrying after them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import time
12
+ from collections import deque
13
+ from dataclasses import dataclass, field
14
+ from typing import Protocol
15
+
16
+
17
+ class RateLimiterProtocol(Protocol):
18
+ """Protocol for rate limiters. Implement this to create custom limiters."""
19
+
20
+ def acquire(self, weight: int = 1) -> float:
21
+ """Acquire permission to make a request.
22
+
23
+ Args:
24
+ weight: Cost of this request (default 1).
25
+
26
+ Returns:
27
+ Time waited in seconds (0 if no wait).
28
+ """
29
+ ...
30
+
31
+ def update_from_headers(self, remaining: int | None, reset_at: int | None) -> None:
32
+ """Update internal state from response headers.
33
+
34
+ Args:
35
+ remaining: Requests remaining in current window.
36
+ reset_at: Unix timestamp when window resets.
37
+ """
38
+ ...
39
+
40
+
41
+ @dataclass
42
+ class RateLimiter:
43
+ """Token bucket rate limiter with sliding window.
44
+
45
+ Thread-safe. Proactively throttles requests to stay under limits.
46
+
47
+ Usage:
48
+ limiter = RateLimiter(requests_per_second=10)
49
+ client = KalshiClient(..., rate_limiter=limiter)
50
+
51
+ # Or manual usage:
52
+ limiter.acquire() # Blocks if needed
53
+ response = make_request()
54
+ limiter.update_from_headers(
55
+ remaining=int(response.headers.get('X-RateLimit-Remaining')),
56
+ reset_at=int(response.headers.get('X-RateLimit-Reset')),
57
+ )
58
+
59
+ Attributes:
60
+ requests_per_second: Target request rate.
61
+ burst: Maximum burst size (default: 2x requests_per_second).
62
+ min_spacing_ms: Minimum ms between requests (anti-burst).
63
+ """
64
+
65
+ requests_per_second: float = 10.0
66
+ burst: int | None = None
67
+ min_spacing_ms: float = 0.0
68
+
69
+ _timestamps: deque = field(default_factory=deque, repr=False)
70
+ _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
71
+ _last_request: float = field(default=0.0, repr=False)
72
+
73
+ # Server-reported state (updated from headers)
74
+ _server_remaining: int | None = field(default=None, repr=False)
75
+ _server_reset_at: int | None = field(default=None, repr=False)
76
+
77
+ def __post_init__(self) -> None:
78
+ if self.burst is None:
79
+ self.burst = max(1, int(self.requests_per_second * 2))
80
+ self._window_size = 1.0 # 1 second sliding window
81
+
82
+ def acquire(self, weight: int = 1) -> float:
83
+ """Block until request is allowed. Returns wait time in seconds."""
84
+ with self._lock:
85
+ now = time.monotonic()
86
+ waited = 0.0
87
+
88
+ # Enforce minimum spacing
89
+ if self.min_spacing_ms > 0:
90
+ elapsed = (now - self._last_request) * 1000
91
+ if elapsed < self.min_spacing_ms:
92
+ sleep_time = (self.min_spacing_ms - elapsed) / 1000
93
+ time.sleep(sleep_time)
94
+ waited += sleep_time
95
+ now = time.monotonic()
96
+
97
+ # Clean old timestamps outside window
98
+ cutoff = now - self._window_size
99
+ while self._timestamps and self._timestamps[0] < cutoff:
100
+ self._timestamps.popleft()
101
+
102
+ # If at capacity, wait for oldest to expire
103
+ while len(self._timestamps) >= self.requests_per_second:
104
+ oldest = self._timestamps[0]
105
+ sleep_time = oldest + self._window_size - now
106
+ if sleep_time > 0:
107
+ time.sleep(sleep_time)
108
+ waited += sleep_time
109
+ now = time.monotonic()
110
+ # Re-clean after sleep
111
+ cutoff = now - self._window_size
112
+ while self._timestamps and self._timestamps[0] < cutoff:
113
+ self._timestamps.popleft()
114
+
115
+ # Check server-reported limits (be conservative)
116
+ if self._server_remaining is not None and self._server_remaining <= 1:
117
+ if self._server_reset_at is not None:
118
+ sleep_until = self._server_reset_at - time.time()
119
+ if sleep_until > 0:
120
+ time.sleep(sleep_until)
121
+ waited += sleep_until
122
+ now = time.monotonic()
123
+ self._server_remaining = None
124
+
125
+ # Record this request
126
+ for _ in range(weight):
127
+ self._timestamps.append(now)
128
+ self._last_request = now
129
+
130
+ return waited
131
+
132
+ def update_from_headers(
133
+ self, remaining: int | None, reset_at: int | None
134
+ ) -> None:
135
+ """Update state from X-RateLimit-Remaining and X-RateLimit-Reset headers."""
136
+ with self._lock:
137
+ if remaining is not None:
138
+ self._server_remaining = remaining
139
+ if reset_at is not None:
140
+ self._server_reset_at = reset_at
141
+
142
+ @property
143
+ def current_rate(self) -> float:
144
+ """Current request rate (requests in last second)."""
145
+ with self._lock:
146
+ now = time.monotonic()
147
+ cutoff = now - self._window_size
148
+ while self._timestamps and self._timestamps[0] < cutoff:
149
+ self._timestamps.popleft()
150
+ return len(self._timestamps)
151
+
152
+ def reset(self) -> None:
153
+ """Clear all state. Useful for testing."""
154
+ with self._lock:
155
+ self._timestamps.clear()
156
+ self._last_request = 0.0
157
+ self._server_remaining = None
158
+ self._server_reset_at = None
159
+
160
+
161
+ @dataclass
162
+ class NoOpRateLimiter:
163
+ """Rate limiter that does nothing. For testing or opt-out."""
164
+
165
+ def acquire(self, weight: int = 1) -> float:
166
+ return 0.0
167
+
168
+ def update_from_headers(
169
+ self, remaining: int | None, reset_at: int | None
170
+ ) -> None:
171
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pykalshi
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A typed Python client for the Kalshi prediction markets API with WebSocket streaming, automatic retries, and ergonomic interfaces
5
5
  Author-email: Arsh Koneru <arshkon@gmail.com>
6
6
  License: MIT
@@ -42,14 +42,14 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
42
42
  Requires-Dist: types-requests>=2.31.0; extra == "dev"
43
43
  Dynamic: license-file
44
44
 
45
- # kalshi-api
45
+ # pykalshi
46
46
 
47
47
  A typed Python client for the [Kalshi](https://kalshi.com) prediction markets API with WebSocket streaming, automatic retries, and ergonomic interfaces.
48
48
 
49
49
  ## Installation
50
50
 
51
51
  ```bash
52
- pip install kalshi-api
52
+ pip install pykalshi
53
53
  ```
54
54
 
55
55
  Create a `.env` file with your credentials from [kalshi.com](https://kalshi.com) → Account & Security → API Keys:
@@ -62,7 +62,7 @@ KALSHI_PRIVATE_KEY_PATH=/path/to/private-key.key
62
62
  ## Quick Start
63
63
 
64
64
  ```python
65
- from kalshi_api import KalshiClient, Action, Side
65
+ from pykalshi import KalshiClient, Action, Side
66
66
 
67
67
  client = KalshiClient()
68
68
  user = client.get_user()
@@ -118,7 +118,7 @@ trades = market.get_trades()
118
118
  ### Orders
119
119
 
120
120
  ```python
121
- from kalshi_api import Action, Side, OrderType
121
+ from pykalshi import Action, Side, OrderType
122
122
 
123
123
  # Limit order (default)
124
124
  order = user.place_order(market, Action.BUY, Side.YES, count=10, price=50)
@@ -134,7 +134,7 @@ order.cancel()
134
134
  Subscribe to live market data via WebSocket:
135
135
 
136
136
  ```python
137
- from kalshi_api import Feed
137
+ from pykalshi import Feed
138
138
 
139
139
  async def main():
140
140
  async with Feed(client) as feed:
@@ -148,7 +148,7 @@ async def main():
148
148
  ### Error Handling
149
149
 
150
150
  ```python
151
- from kalshi_api import InsufficientFundsError, RateLimitError, KalshiAPIError
151
+ from pykalshi import InsufficientFundsError, RateLimitError, KalshiAPIError
152
152
 
153
153
  try:
154
154
  user.place_order(...)
@@ -162,7 +162,7 @@ except KalshiAPIError as e:
162
162
 
163
163
  ## Comparison with Official SDK
164
164
 
165
- | Feature | kalshi-api | kalshi-python (official) |
165
+ | Feature | pykalshi | kalshi-python (official) |
166
166
  |---------|------------|--------------------------|
167
167
  | WebSocket streaming | ✓ | — |
168
168
  | Automatic retry with backoff | ✓ | — |
@@ -0,0 +1,35 @@
1
+ kalshi_api/__init__.py,sha256=H873JDfBga0KEi-G-EJOPBljMZiOTOcpw7x2Qaeru9w,3018
2
+ kalshi_api/api_keys.py,sha256=HMn5bzKo5yX8edcKpJy20NbrR2z7vUa5PJk8TB0iBwU,1922
3
+ kalshi_api/client.py,sha256=4IA-f8OSMFYvxf8tmKL06-2gb7GdDISHDLUPdCtF9ac,19362
4
+ kalshi_api/enums.py,sha256=9YF4ooA1RTprI08-NzqVZoPXISAmHDWgv3j0971BRyI,1127
5
+ kalshi_api/events.py,sha256=0eC9xlqBblwxJdmdiFwjuiL1XUbw6eqcK-vCjoUUgqA,2683
6
+ kalshi_api/exceptions.py,sha256=ikv97ztZwYWKr4SZ0EUTtR50KZQFjbpuI5vMMpPxiW4,3143
7
+ kalshi_api/exchange.py,sha256=39-Zx-VZ6ezkOeB4E9fP4yhTX-qoO4FRSkpcEGmhG60,1359
8
+ kalshi_api/feed.py,sha256=zhPGaER7oBMAHZgpxdppvWDkC-NLn6W_ORnLCkCQh_8,19470
9
+ kalshi_api/markets.py,sha256=ben3yUqpW2Jn1bp67lJ7gyRjUHtwPTZkKQxyH_ao3ZA,6901
10
+ kalshi_api/models.py,sha256=hQdkJMK4sFYXiDEjjEwsEY4skBXEzN3IuJFpzojF8Lw,16038
11
+ kalshi_api/orderbook.py,sha256=40UIqEz-pgM49YdFNTXAd63Rw2hCaEq-sQPPy9l3q_E,4830
12
+ kalshi_api/orders.py,sha256=F4mk0PPS_cBzDRbg0x50GpQb-AqfJPES2Vw45Zes3Rc,3726
13
+ kalshi_api/portfolio.py,sha256=JgkgGTSEytn3OrIRbdci-bvkBOtRmxBK69uFuyYwArI,19937
14
+ kalshi_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ kalshi_api/rate_limiter.py,sha256=UJPjl-YUtLdVwQcaoq97ZH1oeS-IhqtV8K5G6WpUyDg,5901
16
+ pykalshi/__init__.py,sha256=H873JDfBga0KEi-G-EJOPBljMZiOTOcpw7x2Qaeru9w,3018
17
+ pykalshi/api_keys.py,sha256=HMn5bzKo5yX8edcKpJy20NbrR2z7vUa5PJk8TB0iBwU,1922
18
+ pykalshi/client.py,sha256=4IA-f8OSMFYvxf8tmKL06-2gb7GdDISHDLUPdCtF9ac,19362
19
+ pykalshi/enums.py,sha256=9YF4ooA1RTprI08-NzqVZoPXISAmHDWgv3j0971BRyI,1127
20
+ pykalshi/events.py,sha256=0eC9xlqBblwxJdmdiFwjuiL1XUbw6eqcK-vCjoUUgqA,2683
21
+ pykalshi/exceptions.py,sha256=ikv97ztZwYWKr4SZ0EUTtR50KZQFjbpuI5vMMpPxiW4,3143
22
+ pykalshi/exchange.py,sha256=39-Zx-VZ6ezkOeB4E9fP4yhTX-qoO4FRSkpcEGmhG60,1359
23
+ pykalshi/feed.py,sha256=zhPGaER7oBMAHZgpxdppvWDkC-NLn6W_ORnLCkCQh_8,19470
24
+ pykalshi/markets.py,sha256=ben3yUqpW2Jn1bp67lJ7gyRjUHtwPTZkKQxyH_ao3ZA,6901
25
+ pykalshi/models.py,sha256=hQdkJMK4sFYXiDEjjEwsEY4skBXEzN3IuJFpzojF8Lw,16038
26
+ pykalshi/orderbook.py,sha256=40UIqEz-pgM49YdFNTXAd63Rw2hCaEq-sQPPy9l3q_E,4830
27
+ pykalshi/orders.py,sha256=F4mk0PPS_cBzDRbg0x50GpQb-AqfJPES2Vw45Zes3Rc,3726
28
+ pykalshi/portfolio.py,sha256=JgkgGTSEytn3OrIRbdci-bvkBOtRmxBK69uFuyYwArI,19937
29
+ pykalshi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ pykalshi/rate_limiter.py,sha256=UJPjl-YUtLdVwQcaoq97ZH1oeS-IhqtV8K5G6WpUyDg,5901
31
+ pykalshi-0.2.0.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
32
+ pykalshi-0.2.0.dist-info/METADATA,sha256=SVfODyFs1tnfAldVPLhHLe9Jlcx8lxZahQjM4bwU3B0,5589
33
+ pykalshi-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
34
+ pykalshi-0.2.0.dist-info/top_level.txt,sha256=d5Rnt2Wuf3HxyeG6hwGi8-g9G6_KLETpyJ5zExXO1M4,9
35
+ pykalshi-0.2.0.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ pykalshi
@@ -1,20 +0,0 @@
1
- kalshi_api/__init__.py,sha256=H873JDfBga0KEi-G-EJOPBljMZiOTOcpw7x2Qaeru9w,3018
2
- kalshi_api/api_keys.py,sha256=HMn5bzKo5yX8edcKpJy20NbrR2z7vUa5PJk8TB0iBwU,1922
3
- kalshi_api/client.py,sha256=4IA-f8OSMFYvxf8tmKL06-2gb7GdDISHDLUPdCtF9ac,19362
4
- kalshi_api/enums.py,sha256=9YF4ooA1RTprI08-NzqVZoPXISAmHDWgv3j0971BRyI,1127
5
- kalshi_api/events.py,sha256=0eC9xlqBblwxJdmdiFwjuiL1XUbw6eqcK-vCjoUUgqA,2683
6
- kalshi_api/exceptions.py,sha256=ikv97ztZwYWKr4SZ0EUTtR50KZQFjbpuI5vMMpPxiW4,3143
7
- kalshi_api/exchange.py,sha256=39-Zx-VZ6ezkOeB4E9fP4yhTX-qoO4FRSkpcEGmhG60,1359
8
- kalshi_api/feed.py,sha256=zhPGaER7oBMAHZgpxdppvWDkC-NLn6W_ORnLCkCQh_8,19470
9
- kalshi_api/markets.py,sha256=ben3yUqpW2Jn1bp67lJ7gyRjUHtwPTZkKQxyH_ao3ZA,6901
10
- kalshi_api/models.py,sha256=hQdkJMK4sFYXiDEjjEwsEY4skBXEzN3IuJFpzojF8Lw,16038
11
- kalshi_api/orderbook.py,sha256=40UIqEz-pgM49YdFNTXAd63Rw2hCaEq-sQPPy9l3q_E,4830
12
- kalshi_api/orders.py,sha256=F4mk0PPS_cBzDRbg0x50GpQb-AqfJPES2Vw45Zes3Rc,3726
13
- kalshi_api/portfolio.py,sha256=JgkgGTSEytn3OrIRbdci-bvkBOtRmxBK69uFuyYwArI,19937
14
- kalshi_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- kalshi_api/rate_limiter.py,sha256=UJPjl-YUtLdVwQcaoq97ZH1oeS-IhqtV8K5G6WpUyDg,5901
16
- pykalshi-0.1.0.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
17
- pykalshi-0.1.0.dist-info/METADATA,sha256=qYNSSrd3cUElCZt-FuL0MoSifw7NdKoQlx6lpjcfYRo,5603
18
- pykalshi-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
19
- pykalshi-0.1.0.dist-info/top_level.txt,sha256=j1cmqf7vUVoke4KfEg_he913wTsgN5nT0Ky5P3P7Dik,11
20
- pykalshi-0.1.0.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- kalshi_api