Python SDK
Errors
The exception hierarchy, how a reply maps onto it, what each exception carries, and how to catch by class.
Every error the SDK raises descends from SahiBaseException, so one except
clause can handle anything it might raise. Below that the tree splits three
ways, by where the problem is:
SahiConfigurationException: the SDK was used wrongly before any request was made. A token of the wrong shape, a modify with nothing to change. Raised locally, never from a server reply.SahiAPIException: a request reached a Sahi host and came back non-2xx, or never came back. It carries the server'sstatus,codeandmessage, therequest_idto quote to support, and ahintsaying what to do.SahiFeedException: a WebSocket stream failed to connect, was closed by the server for good, or was asked for a value it does not hold.
Catch by class, not by code or message. The auth host (apis.sahi.com)
and the API host (direct.sahi.com) spell the same code differently
(Unauthorized, UNAUTHORIZED); the class is the same for both.
The SDK also raises three plain Python exceptions, before sending anything:
ValueError: an empty id list, an unknown candle size, ato_msbeforefrom_ms, or an unknown tick-stream mode.TypeError: a basket leg missing a required key, or carrying one the API does not define.RuntimeError: a call on a client afterclose().
The tree#
SahiBaseExceptionSahiConfigurationExceptionSahiAPIExceptionSahiBadRequestException(400)SahiValidationException(VALIDATION_FAILED)
SahiAuthenticationException(401)SahiAccountLockedException(Locked)
SahiAuthorisationException(403)SahiKeyNotApprovedException(403 on the auth host)SahiIpNotAllowedException(IP_NOT_ALLOWED)
SahiNotFoundException(404)SahiOrderNotFoundException(ORDER_NOT_FOUND)
SahiInvalidOrderStateException(409INVALID_ORDER_STATE)SahiRateLimitException(429)SahiServerException(5xx)SahiMaintenanceException(503UNDER_MAINTENANCE)
SahiTimeoutException(no reply in time)SahiConnectionException(could not connect)
SahiFeedExceptionSahiFeedConnectionExceptionSahiFeedAuthExpiredExceptionSahiFeedForbiddenExceptionSahiFeedNotSubscribedException
SahiKeyNotApprovedException is a subclass of SahiAuthorisationException, so
an except SahiAuthorisationException around a mint and a call catches both:
catch SahiKeyNotApprovedException first if you handle them differently.
How a reply becomes an exception#
A known code decides the class whatever the status; otherwise the status and
the host do. Anything not in the table becomes SahiServerException for a 5xx
and SahiAPIException otherwise. A 4xx without the API's error envelope was
refused by the edge before it reached the API, usually for an over-long URL,
and is a plain SahiAPIException.
| Status | Host | Code | Class |
|---|---|---|---|
| any | any | VALIDATION_FAILED |
SahiValidationException |
| any | any | ORDER_NOT_FOUND |
SahiOrderNotFoundException |
| any | any | INVALID_ORDER_STATE |
SahiInvalidOrderStateException |
| any | any | IP_NOT_ALLOWED |
SahiIpNotAllowedException |
| any | any | Locked |
SahiAccountLockedException |
| any | any | UNDER_MAINTENANCE |
SahiMaintenanceException |
4xx |
edge | no envelope | SahiAPIException |
400 |
any | other | SahiBadRequestException |
401 |
any | other | SahiAuthenticationException |
403 |
auth | other | SahiKeyNotApprovedException |
403 |
API | other | SahiAuthorisationException |
404 |
any | other | SahiNotFoundException |
429 |
any | any | SahiRateLimitException |
5xx |
any | any | SahiServerException |
What an API exception carries#
| Attribute | Type | Meaning |
|---|---|---|
status |
int or None | The HTTP status, or None when no reply arrived. |
code |
str or None | The envelope's code, verbatim as the host sent it. |
message |
str or None | The server's message. |
request_id |
str or None | The id to quote to Sahi support: the server's when it returned one, otherwise the one the SDK sent. |
hint |
str | What to do about it, per class. |
str(exc) combines all of them, so logging the exception is enough to
diagnose and act on it:
HTTP 403 IP_NOT_ALLOWED: order writes must come from a registered IP. Order writes must come from a static IP registered on your account. Check the public egress address of this host — it usually means the address changed. [request_id=sahiapi-5f1c...]
SahiRateLimitException carries one thing more: retry_after, seconds to
wait, from the Retry-After header, or None on the auth host, which sends
none; wait for the next minute there.
A feed exception carries close_code instead: the WebSocket close code the
server sent (4401, 4403 or 4429; the SahiAPI.WS_CLOSE_* constants name
them), or None when there was none, such as a refused handshake.
Retries#
The SDK retries one case itself: a GET that gets 429 with a Retry-After
of five seconds or less is retried once, after that wait. Everything else is
raised for you to handle:
- a longer
Retry-After, from a per-minute or per-day limit; - any
POST, the margin checks included; - any order write, since a write that timed out may have reached the exchange.
Catching by class#
Catch the class you can act on and let the rest propagate. Around the mint:
from sahiapi import SahiAPI, SahiAuthenticationException, SahiKeyNotApprovedException
try:
api = SahiAPI.from_credentials(key_id="pk_...", secret="sahi_sk_...")
except SahiKeyNotApprovedException:
print("approve the key for today in your Sahi account, then run again")
raise
except SahiAuthenticationException as e:
print("sign-in failed:", e) # wrong secret, revoked key, locked account
raise
Around an order write, wrap the place_order call of Getting started's
Place and cancel an order like this:
from sahiapi import (
SahiIpNotAllowedException,
SahiRateLimitException,
SahiTimeoutException,
SahiValidationException,
)
try:
result = api.place_order(...) # the same arguments as in Getting started
except SahiIpNotAllowedException:
print("this machine's public IP is not registered on the account")
except SahiValidationException as e:
print("refused:", e.message)
except SahiRateLimitException as e:
print("over the limit; wait", e.retry_after, "seconds")
except SahiTimeoutException:
print("it may have gone through; check api.get_order_list() before resending")
For a stream, the exception comes out of consume():
from sahiapi import SahiFeed, SahiFeedAuthExpiredException
feed = SahiFeed(api)
feed.subscribe_ltp([instrument_id], on_data_received=lambda data, meta: print(data))
try:
feed.consume()
except SahiFeedAuthExpiredException:
print("the token expired at 06:00 IST; mint a new one and build a new feed")
Every class#
SahiBaseException#
Base class for everything the SDK raises.
SahiConfigurationException#
The SDK was misconfigured or misused before a request could be made: a
SahiAPI() or set_access_token() given something other than the token
response or its access_token string, a value passed to
sahiapi.token_expires_at() or sahiapi.token_scopes() that is not a JWT, or a
modify_order with nothing to change. The message says
what was wrong and what to pass instead.
SahiAPIException#
A request to a Sahi host failed. Usually a non-2xx reply; the two subclasses
SahiConnectionException and SahiTimeoutException cover the cases where no
reply arrived. Also raised directly for a failed scrip-master download, with
status set when the file host answered.
SahiBadRequestException#
400: the request was malformed. On the auth host this also covers scopes
that are not a subset of the key's.
SahiValidationException#
VALIDATION_FAILED, as 400 for a malformed field or 422 for one the API
rejects on meaning: an unknown symbol, a quantity over the freeze quantity, a
contract past expiry, a REGULAR order while the market is closed. The server
message names the field or rule.
SahiAuthenticationException#
401: the credential or token was missing, unknown, wrong, revoked or
expired. The auth host deliberately does not say which. When a /v1 call
raises this, mint a new token.
SahiAccountLockedException#
401 Locked: the account behind the credential is frozen or locked. Contact
Sahi support.
SahiAuthorisationException#
403 under /v1: the token is valid but lacks the scope the endpoint needs.
Scopes are fixed when the key is created and a token can only narrow them.
SahiKeyNotApprovedException#
403 on the auth host: the key is not approved for the current trading day.
Approve it in your Sahi account and mint again.
SahiIpNotAllowedException#
403 IP_NOT_ALLOWED: an order write did not come from a registered IP. Reads
and market data are not IP-checked. It usually means this machine's public
address changed.
SahiNotFoundException#
404: the route or resource does not exist. Also returned by the funds,
margin and profile endpoints when the account behind the token is not yet set
up for trading.
SahiOrderNotFoundException#
404 ORDER_NOT_FOUND: no such order on today's book for this account.
SahiInvalidOrderStateException#
409 INVALID_ORDER_STATE: only Open and OpenPartial orders can be modified or
cancelled; a completed, cancelled or rejected order is final.
SahiRateLimitException#
429: a rate-limit window was exhausted. retry_after is the seconds the
window has left, or None on the auth host. Batch requests rather than looping
per instrument.
SahiServerException#
5xx: a failure on Sahi's side, usually code INTERNAL. Retry a read
after a short wait; check the order book before retrying an order write. If a
read keeps failing, write to developers@sahi.com
with the request_id.
SahiMaintenanceException#
503 UNDER_MAINTENANCE: the margin checks are closed for the daily 04:45–07:00
IST start-of-day processing. A subclass of SahiServerException. Retry after
07:00 IST.
SahiTimeoutException#
No reply arrived within the timeout. The request may or may not have reached the server. For an order write, look for the order before resending it: Find the order after a timeout.
SahiConnectionException#
The connection to the host could not be opened: DNS, connection refused, a TLS error, a connect timeout. Nothing was sent, so the request is safe to retry once the network is fixed.
SahiFeedException#
Base class for stream errors; carries close_code.
SahiFeedConnectionException#
A connection-level refusal: close code 4429 when the account or token
already has five connections open, which clears once another closes; or
close_code None for a send attempted while the stream is not connected.
A heartbeat drop, a slow-consumer drop or a network drop is not this
exception: those reconnect on their own.
SahiFeedAuthExpiredException#
The token was rejected: close code 4401, or 401 at the handshake. Mint a
fresh token, build a new feed and subscribe again.
SahiFeedForbiddenException#
The token lacks the stream's scope: 403 at the handshake, or close 4403.
SahiFeedNotSubscribedException#
A cached value was requested for an instrument that is not subscribed.