pykalshi 0.1.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,115 @@
1
+ from typing import Any
2
+
3
+
4
+ class KalshiError(Exception):
5
+ """Base exception for all Kalshi API errors."""
6
+
7
+ retryable: bool = False
8
+
9
+
10
+ class KalshiAPIError(KalshiError):
11
+ """Raised when the API returns a non-200 response.
12
+
13
+ Attributes:
14
+ status_code: HTTP status code.
15
+ message: Error message from the API.
16
+ error_code: Kalshi-specific error code (e.g., "insufficient_balance").
17
+ method: HTTP method of the failed request.
18
+ endpoint: API endpoint path.
19
+ request_body: Request payload (for POST/PUT), if available.
20
+ response_body: Raw response body for debugging.
21
+ retryable: Whether this error is safe to retry (e.g., 5xx, rate limits).
22
+ """
23
+
24
+ retryable = True # 5xx errors are generally retryable
25
+
26
+ def __init__(
27
+ self,
28
+ status_code: int,
29
+ message: str,
30
+ error_code: str | None = None,
31
+ *,
32
+ method: str | None = None,
33
+ endpoint: str | None = None,
34
+ request_body: dict[str, Any] | None = None,
35
+ response_body: dict[str, Any] | str | None = None,
36
+ ):
37
+ # Build informative message
38
+ parts = [f"{status_code}: {message}"]
39
+ if error_code:
40
+ parts[0] = f"{status_code}: {message} ({error_code})"
41
+ if method and endpoint:
42
+ parts.append(f"[{method} {endpoint}]")
43
+
44
+ super().__init__(" ".join(parts))
45
+
46
+ self.status_code = status_code
47
+ self.message = message
48
+ self.error_code = error_code
49
+ self.method = method
50
+ self.endpoint = endpoint
51
+ self.request_body = request_body
52
+ self.response_body = response_body
53
+
54
+ def __repr__(self) -> str:
55
+ return (
56
+ f"{self.__class__.__name__}("
57
+ f"status_code={self.status_code}, "
58
+ f"message={self.message!r}, "
59
+ f"error_code={self.error_code!r}, "
60
+ f"endpoint={self.endpoint!r})"
61
+ )
62
+
63
+
64
+ class AuthenticationError(KalshiAPIError):
65
+ """Raised when authentication fails (401/403).
66
+
67
+ Common causes:
68
+ - Invalid or expired API key
69
+ - Malformed signature
70
+ - Clock skew (timestamp too old)
71
+ """
72
+
73
+ retryable = False
74
+
75
+
76
+ class InsufficientFundsError(KalshiAPIError):
77
+ """Raised when the order cannot be placed due to insufficient funds.
78
+
79
+ Check `request_body` for the order that was rejected.
80
+ """
81
+
82
+ retryable = False
83
+
84
+
85
+ class ResourceNotFoundError(KalshiAPIError):
86
+ """Raised when a resource (market, order) is not found (404).
87
+
88
+ Check `endpoint` to see which resource was not found.
89
+ """
90
+
91
+ retryable = False
92
+
93
+
94
+ class RateLimitError(KalshiAPIError):
95
+ """Raised when rate limit retries are exhausted (429).
96
+
97
+ Consider using a RateLimiter to proactively throttle requests.
98
+ """
99
+
100
+ retryable = True
101
+
102
+
103
+ class OrderRejectedError(KalshiAPIError):
104
+ """Raised when an order is rejected by the exchange.
105
+
106
+ Common causes:
107
+ - Market is closed or settled
108
+ - Invalid price (outside 1-99 range)
109
+ - Self-trade prevention triggered
110
+ - Post-only order would take liquidity
111
+
112
+ Check `request_body` for the rejected order details.
113
+ """
114
+
115
+ retryable = False
kalshi_api/exchange.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, Any
3
+ from .models import ExchangeStatus, Announcement
4
+
5
+ if TYPE_CHECKING:
6
+ from .client import KalshiClient
7
+
8
+
9
+ class Exchange:
10
+ """Exchange status, schedule, and announcements."""
11
+
12
+ def __init__(self, client: KalshiClient) -> None:
13
+ self._client = client
14
+
15
+ def get_status(self) -> ExchangeStatus:
16
+ """Get current exchange operational status."""
17
+ data = self._client.get("/exchange/status")
18
+ return ExchangeStatus.model_validate(data)
19
+
20
+ def is_trading(self) -> bool:
21
+ """Quick check if trading is currently active."""
22
+ return self.get_status().trading_active
23
+
24
+ def get_schedule(self) -> dict[str, Any]:
25
+ """Get exchange trading schedule (raw format)."""
26
+ data = self._client.get("/exchange/schedule")
27
+ return data.get("schedule", {})
28
+
29
+ def get_announcements(self) -> list[Announcement]:
30
+ """Get exchange-wide announcements."""
31
+ data = self._client.get("/exchange/announcements")
32
+ return [Announcement.model_validate(a) for a in data.get("announcements", [])]
33
+
34
+ def get_user_data_timestamp(self) -> int:
35
+ """Get timestamp of last user data validation (Unix ms)."""
36
+ data = self._client.get("/exchange/user_data_timestamp")
37
+ return data.get("user_data_timestamp", 0)