Skip to content

wisdom_capital

Classes:

Name Description
WisdomCapitalAPIException

Raised for Wisdom Capital broker API errors.

WisdomCapitalAPI

Wisdom Capital API class

WisdomCapitalAPIException

WisdomCapitalAPIException(code=None, message=None)

Bases: BrokerAPIException

Raised for Wisdom Capital broker API errors.

Example programs

examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPIException/example_1_catching_a_refused_token.py
"""Catches the `WisdomCapitalAPIException` that `WisdomCapitalAPI` raises when Wisdom Capital refuses the access token.

When Wisdom Capital answers with an HTTP error, `WisdomCapitalAPI`'s `_request` raises `WisdomCapitalAPIException` instead of returning an answer. Symphony XTS names the error in `code` and explains it in `description`, and `_request` copies both into the exception.

The real constructor reads the broker's settings from MongoDB, probes `GET /interactive/user/balance` and the market data application's `clientConfig`, and logs in to Wisdom Capital with a session request for the interactive application and a second login for the market data application when the stored token no longer works. This program must never log in, so a small subclass, `WisdomCapitalAPIWithoutLogin`, skips that constructor and is handed the settings and a stand-in for Redis that already holds the stored login. `requests.request` is replaced, only while each request is sent, by a stand-in server that records the request and answers with a recorded-looking Wisdom Capital response, so nothing reaches the network. Here the stand-in server answers with the HTTP 400 Wisdom Capital sends for a token it no longer accepts.

Notice that `code` holds Wisdom Capital's own error code rather than the HTTP status, so a caller can tell an expired token from other refusals.

Run it from the project root:

    python examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPIException/example_1_catching_a_refused_token.py
"""

import json
import logging
import unittest.mock

import requests

from stock_brokers.api.wisdom_capital import (
    WisdomCapitalAPI,
    WisdomCapitalAPIException,
)

STORED_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'morning-token',
    'last_login': '2026-09-30 07:00:05',
}

REFUSAL = {
    'type': 'error',
    'code': 'e-token-0002',
    'description': 'Please Provide token to Authenticate',
}


class SharedLoginCache:
    """A dictionary-backed stand-in for Redis, holding the `last_login` hash that every process reads its token from.

    Attributes:
        hashes (dict): Each hash name mapped to a dictionary of its fields.
    """

    def __init__(self):
        """Starts with nothing stored.

        Returns:
            None: This method returns nothing.
        """
        self.hashes = {}

    def hget(self, name, key):
        """Reads one field of a hash.

        Args:
            name (str): The hash name.
            key (str): The field name.

        Returns:
            str | None: The stored value, or None when there is none.
        """
        fields = self.hashes.get(name, {})
        return fields.get(key)

    def hset(self, name, key, value):
        """Writes one field of a hash, as a login does.

        Args:
            name (str): The hash name.
            key (str): The field name.
            value (str): The value to store.

        Returns:
            int: Always 1, the number of fields Redis reports written.
        """
        if name not in self.hashes:
            self.hashes[name] = {}
        self.hashes[name][key] = value
        return 1


class RecordedBrokerServer:
    """A stand-in for `requests.request` that answers every call with one recorded Wisdom Capital response and remembers what was sent.

    Attributes:
        status_code (int): The HTTP status every answer carries.
        content_type (str): The Content-Type header every answer carries.
        body (str): The body every answer carries.
        calls (list): One dictionary per request, holding its method, URL, headers, body and certificate setting.
    """

    def __init__(self, status_code, content_type, body):
        """Holds the answer to give.

        Args:
            status_code (int): The HTTP status every answer carries.
            content_type (str): The Content-Type header every answer carries.
            body (str): The body every answer carries.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.content_type = content_type
        self.body = body
        self.calls = []

    def request(self, method, url, **options):
        """Records one request and answers it, in place of `requests.request`.

        Args:
            method (str): The HTTP method.
            url (str): The URL requested.
            **options (Any): The remaining `requests.request` arguments, such as `headers` and `data`.

        Returns:
            requests.Response: The recorded answer.
        """
        self.calls.append({
            'method': method,
            'url': url,
            'headers': options.get('headers'),
            'data': options.get('data'),
            'verify': options.get('verify'),
        })
        response = requests.Response()
        response.status_code = self.status_code
        response.headers['Content-Type'] = self.content_type
        response._content = self.body.encode('utf-8')
        response.url = url
        return response


class WisdomCapitalAPIWithoutLogin(WisdomCapitalAPI):
    """A `WisdomCapitalAPI` that is handed its settings and cache instead of loading them and logging in.

    The real constructor reads the `wisdom_capital` settings document from MongoDB and then logs in to Wisdom Capital if the stored token no longer works. This subclass sets the same attributes from what it is given and does nothing else, so every request it makes still runs `WisdomCapitalAPI`'s own `_request`.
    """

    def __init__(self, cache, settings, stored_login):
        """Sets the attributes the real constructor would have loaded.

        MongoDB is left out because the stored login is handed in here and is already in the cache, which is the first place `_current_login` looks.

        Args:
            cache (SharedLoginCache): The stand-in for Redis, already holding the stored login.
            settings (dict): The broker's settings document.
            stored_login (dict): The broker's `last_login` document, as the real constructor reads it from MongoDB.

        Returns:
            None: This method returns nothing.
        """
        self._broker_name = 'wisdom_capital'
        self._cache = cache
        self._mongo_db = None
        self._logger = logging.getLogger('wisdom_capital')
        self._settings = settings
        self._last_login = stored_login
        self.market_data_session_error = None


class CatchingARefusedTokenExample:
    """Sends a request that Wisdom Capital refuses and prints the exception it raises.

    Attributes:
        server (RecordedBrokerServer): The stand-in for Wisdom Capital's API, answering with a refusal.
        api (WisdomCapitalAPIWithoutLogin): The API object being shown.
    """

    def __init__(self):
        """Stores an expired login and builds the API object and the stand-in server.

        Returns:
            None: This method returns nothing.
        """
        cache = SharedLoginCache()
        cache.hset('last_login', 'wisdom_capital', json.dumps(STORED_LOGIN))
        settings = {
            'ucc_code': 'WC0001',
        }
        self.server = RecordedBrokerServer(400, 'application/json', json.dumps(REFUSAL))
        self.api = WisdomCapitalAPIWithoutLogin(cache, settings, STORED_LOGIN)

    def run(self):
        """Sends the request, catches the exception and prints its fields.

        Returns:
            None: This method returns nothing.
        """
        print(f'Wisdom Capital answers HTTP {self.server.status_code} with: {self.server.body}')
        with unittest.mock.patch('requests.request', self.server.request):
            try:
                self.api.get(url='https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001')
            except WisdomCapitalAPIException as error:
                print(f'Caught: {type(error).__name__}')
                print(f'code: {error.code}')
                print(f'message: {error.message}')


if __name__ == '__main__':
    CatchingARefusedTokenExample().run()

It prints:

Wisdom Capital answers HTTP 400 with: {"type": "error", "code": "e-token-0002", "description": "Please Provide token to Authenticate"}
Caught: WisdomCapitalAPIException
code: e-token-0002
message: Please Provide token to Authenticate
examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPIException/example_2_catching_a_gateway_error.py
"""Catches a gateway error from Wisdom Capital through the base class that every broker's exception shares.

A load balancer in front of Wisdom Capital's API sometimes answers with an HTML page instead of JSON, for example a 502 Bad Gateway while the service behind it restarts. `WisdomCapitalAPI`'s `_request` cannot read an error code from such a page, so it raises `WisdomCapitalAPIException` with the HTTP status as `code` and the page's text as `message`.

Every broker's exception class derives from `BrokerAPIException`, so code that talks to several brokers can catch that one class and still read `code` and `message`. This program catches it that way.

The real constructor reads the broker's settings from MongoDB, probes `GET /interactive/user/balance` and the market data application's `clientConfig`, and logs in to Wisdom Capital with a session request for the interactive application and a second login for the market data application when the stored token no longer works. This program must never log in, so a small subclass, `WisdomCapitalAPIWithoutLogin`, skips that constructor and is handed the settings and a stand-in for Redis that already holds the stored login. `requests.request` is replaced, only while each request is sent, by a stand-in server that records the request and answers with a recorded-looking Wisdom Capital response, so nothing reaches the network.

Notice that the exception caught as `BrokerAPIException` is still a `WisdomCapitalAPIException`, and that its `code` is the integer 502 rather than a code of Wisdom Capital's own.

Run it from the project root:

    python examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPIException/example_2_catching_a_gateway_error.py
"""

import json
import logging
import unittest.mock

import requests

from stock_brokers.api.base import (
    BrokerAPIException,
)
from stock_brokers.api.wisdom_capital import (
    WisdomCapitalAPI,
    WisdomCapitalAPIException,
)

STORED_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'morning-token',
    'last_login': '2026-09-30 07:00:05',
}

GATEWAY_PAGE = '<html><head><title>502 Bad Gateway</title></head><body><center><h1>502 Bad Gateway</h1></center></body></html>'


class SharedLoginCache:
    """A dictionary-backed stand-in for Redis, holding the `last_login` hash that every process reads its token from.

    Attributes:
        hashes (dict): Each hash name mapped to a dictionary of its fields.
    """

    def __init__(self):
        """Starts with nothing stored.

        Returns:
            None: This method returns nothing.
        """
        self.hashes = {}

    def hget(self, name, key):
        """Reads one field of a hash.

        Args:
            name (str): The hash name.
            key (str): The field name.

        Returns:
            str | None: The stored value, or None when there is none.
        """
        fields = self.hashes.get(name, {})
        return fields.get(key)

    def hset(self, name, key, value):
        """Writes one field of a hash, as a login does.

        Args:
            name (str): The hash name.
            key (str): The field name.
            value (str): The value to store.

        Returns:
            int: Always 1, the number of fields Redis reports written.
        """
        if name not in self.hashes:
            self.hashes[name] = {}
        self.hashes[name][key] = value
        return 1


class RecordedBrokerServer:
    """A stand-in for `requests.request` that answers every call with one recorded Wisdom Capital response and remembers what was sent.

    Attributes:
        status_code (int): The HTTP status every answer carries.
        content_type (str): The Content-Type header every answer carries.
        body (str): The body every answer carries.
        calls (list): One dictionary per request, holding its method, URL, headers, body and certificate setting.
    """

    def __init__(self, status_code, content_type, body):
        """Holds the answer to give.

        Args:
            status_code (int): The HTTP status every answer carries.
            content_type (str): The Content-Type header every answer carries.
            body (str): The body every answer carries.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.content_type = content_type
        self.body = body
        self.calls = []

    def request(self, method, url, **options):
        """Records one request and answers it, in place of `requests.request`.

        Args:
            method (str): The HTTP method.
            url (str): The URL requested.
            **options (Any): The remaining `requests.request` arguments, such as `headers` and `data`.

        Returns:
            requests.Response: The recorded answer.
        """
        self.calls.append({
            'method': method,
            'url': url,
            'headers': options.get('headers'),
            'data': options.get('data'),
            'verify': options.get('verify'),
        })
        response = requests.Response()
        response.status_code = self.status_code
        response.headers['Content-Type'] = self.content_type
        response._content = self.body.encode('utf-8')
        response.url = url
        return response


class WisdomCapitalAPIWithoutLogin(WisdomCapitalAPI):
    """A `WisdomCapitalAPI` that is handed its settings and cache instead of loading them and logging in.

    The real constructor reads the `wisdom_capital` settings document from MongoDB and then logs in to Wisdom Capital if the stored token no longer works. This subclass sets the same attributes from what it is given and does nothing else, so every request it makes still runs `WisdomCapitalAPI`'s own `_request`.
    """

    def __init__(self, cache, settings, stored_login):
        """Sets the attributes the real constructor would have loaded.

        MongoDB is left out because the stored login is handed in here and is already in the cache, which is the first place `_current_login` looks.

        Args:
            cache (SharedLoginCache): The stand-in for Redis, already holding the stored login.
            settings (dict): The broker's settings document.
            stored_login (dict): The broker's `last_login` document, as the real constructor reads it from MongoDB.

        Returns:
            None: This method returns nothing.
        """
        self._broker_name = 'wisdom_capital'
        self._cache = cache
        self._mongo_db = None
        self._logger = logging.getLogger('wisdom_capital')
        self._settings = settings
        self._last_login = stored_login
        self.market_data_session_error = None


class CatchingAGatewayErrorExample:
    """Sends a request that a gateway answers with an HTML error page and catches the result as a `BrokerAPIException`.

    Attributes:
        server (RecordedBrokerServer): The stand-in for Wisdom Capital's API, answering with a gateway error.
        api (WisdomCapitalAPIWithoutLogin): The API object being shown.
    """

    def __init__(self):
        """Stores a login and builds the API object and the stand-in server.

        Returns:
            None: This method returns nothing.
        """
        cache = SharedLoginCache()
        cache.hset('last_login', 'wisdom_capital', json.dumps(STORED_LOGIN))
        settings = {
            'ucc_code': 'WC0001',
        }
        self.server = RecordedBrokerServer(502, 'text/html; charset=utf-8', GATEWAY_PAGE)
        self.api = WisdomCapitalAPIWithoutLogin(cache, settings, STORED_LOGIN)

    def run(self):
        """Sends the request, catches the exception through the base class and prints what it holds.

        Returns:
            None: This method returns nothing.
        """
        with unittest.mock.patch('requests.request', self.server.request):
            try:
                self.api.get(url='https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001')
            except BrokerAPIException as error:
                print(f'Caught as BrokerAPIException: {type(error).__name__}')
                print(f'Is an instance of WisdomCapitalAPIException: {isinstance(error, WisdomCapitalAPIException)}')
                print(f'code: {error.code!r}')
                print(f'message: {error.message}')


if __name__ == '__main__':
    CatchingAGatewayErrorExample().run()

It prints:

Caught as BrokerAPIException: WisdomCapitalAPIException
Is an instance of WisdomCapitalAPIException: True
code: 502
message: <html><head><title>502 Bad Gateway</title></head><body><center><h1>502 Bad Gateway</h1></center></body></html>

BrokerAPIException represents an exception condition related to Broker API calls

-code: (string) error code -message: (string) error message

Example programs

examples/stock_brokers/api/base/BrokerAPIException/example_1_raising_and_reading_an_error.py
"""Raises a `BrokerAPIException` and reads the code and message it carries.

`BrokerAPIException` is what every broker class raises, through its own subclass, when a broker refuses a request. It holds the broker's error `code` and a `message`, both optional, and passes both to `Exception` as well, so they also appear in `args` and in the exception's text.

This program raises one with a Kite-style code and message, as `ZerodhaAPI` does for an expired token, and one with neither, and prints what a handler can read from each. It needs no data store, no network and no broker.

Notice that the code and message are plain attributes, and that an exception raised without them still has both, set to None.

Run it from the project root:

    python examples/stock_brokers/api/base/BrokerAPIException/example_1_raising_and_reading_an_error.py
"""

from stock_brokers.api.base import (
    BrokerAPIException,
)


class RaisingAndReadingAnErrorExample:
    """Raises two exceptions and prints what each holds.

    Attributes:
        errors (list): The exceptions to raise, one with a code and message and one without.
    """

    def __init__(self):
        """Builds the exceptions to raise.

        Returns:
            None: This method returns nothing.
        """
        self.errors = [
            BrokerAPIException(code='TokenException', message='Incorrect `api_key` or `access_token`.'),
            BrokerAPIException(),
        ]

    def run(self):
        """Raises each exception, catches it and prints its fields.

        Returns:
            None: This method returns nothing.
        """
        for error_to_raise in self.errors:
            try:
                raise error_to_raise
            except BrokerAPIException as error:
                print(f'code: {error.code!r}')
                print(f'message: {error.message!r}')
                print(f'args: {error.args!r}')
                print(f'text: {error}')
                print()


if __name__ == '__main__':
    RaisingAndReadingAnErrorExample().run()

It prints:

code: 'TokenException'
message: 'Incorrect `api_key` or `access_token`.'
args: ('TokenException', 'Incorrect `api_key` or `access_token`.')
text: ('TokenException', 'Incorrect `api_key` or `access_token`.')

code: None
message: None
args: (None, None)
text: (None, None)
examples/stock_brokers/api/base/BrokerAPIException/example_2_one_handler_for_every_broker.py
"""Handles the errors of several brokers with one `except BrokerAPIException` clause.

Each broker module defines its own exception class, such as `DhanAPIException` or `ZerodhaAPIException`, and every one of them derives from `BrokerAPIException`. Code that works across brokers, such as a script that polls every broker's funds, can therefore catch the base class once and still read the broker's `code` and `message`, and use the exception's class to say which broker refused.

This program raises three brokers' exceptions with the codes and messages those brokers send for an expired token, catches them all with one handler, and prints what the handler sees. It needs no data store, no network and no broker.

Notice that every exception is caught by the same clause, and that each keeps its broker's own code: Dhan's `errorType`, Kite's `error_type` and Noren's `stat`.

Run it from the project root:

    python examples/stock_brokers/api/base/BrokerAPIException/example_2_one_handler_for_every_broker.py
"""

from stock_brokers.api.base import (
    BrokerAPIException,
)
from stock_brokers.api.dhan import (
    DhanAPIException,
)
from stock_brokers.api.flattrade import (
    FlattradeAPIException,
)
from stock_brokers.api.zerodha import (
    ZerodhaAPIException,
)


class RefusingBroker:
    """A stand-in for one broker's API object whose every request is refused with that broker's exception.

    Attributes:
        broker_name (str): The broker's name.
        refusal (BrokerAPIException): The exception every request raises.
    """

    def __init__(self, broker_name, refusal):
        """Holds the broker's name and the refusal.

        Args:
            broker_name (str): The broker's name.
            refusal (BrokerAPIException): The exception every request raises.

        Returns:
            None: This method returns nothing.
        """
        self.broker_name = broker_name
        self.refusal = refusal

    def get(self, url):
        """Refuses the request.

        Args:
            url (str): The URL requested.

        Returns:
            dict: Never returns.

        Raises:
            BrokerAPIException: Always, the broker's own subclass of it.
        """
        raise self.refusal


class OneHandlerExample:
    """Asks every broker for its funds and handles every refusal in one place.

    Attributes:
        brokers (list): The stand-in brokers to ask.
    """

    def __init__(self):
        """Builds the refusing brokers.

        Returns:
            None: This method returns nothing.
        """
        self.brokers = [
            RefusingBroker('dhan', DhanAPIException(code='Invalid_Authentication', message='Client ID or user generated access token is invalid or expired.')),
            RefusingBroker('zerodha', ZerodhaAPIException(code='TokenException', message='Incorrect `api_key` or `access_token`.')),
            RefusingBroker('flattrade', FlattradeAPIException(code='Not_Ok', message='Session Expired :  Invalid Session Key')),
        ]

    def run(self):
        """Sends one request to each broker and prints each refusal.

        Returns:
            None: This method returns nothing.
        """
        for broker in self.brokers:
            try:
                broker.get(url='/funds')
            except BrokerAPIException as error:
                print(f'{broker.broker_name}: {type(error).__name__} code={error.code} message={error.message}')


if __name__ == '__main__':
    OneHandlerExample().run()

It prints:

dhan: DhanAPIException code=Invalid_Authentication message=Client ID or user generated access token is invalid or expired.
zerodha: ZerodhaAPIException code=TokenException message=Incorrect `api_key` or `access_token`.
flattrade: FlattradeAPIException code=Not_Ok message=Session Expired :  Invalid Session Key
Source code in stock_brokers/api/base.py
def __init__(self, code=None, message=None):
    """
    BrokerAPIException represents an exception condition related to Broker  API calls

    -`code`: (string) error code
    -`message`: (string) error message
    """
    super(BrokerAPIException, self).__init__(code, message)
    self.code = code
    self.message = message

WisdomCapitalAPI

WisdomCapitalAPI()

Bases: BrokerAPI

Wisdom Capital API class

Establishes both of the sessions Wisdom Capital's platform needs, logging in only where the stored token has stopped working.

Symphony XTS splits a broker into two applications with separate credentials. The interactive application places orders and reports the account, and the market data application serves quotes and charts; each issues its own token and refuses the other's. Constructing this class establishes both, and leaves them in the last_login document as access_token and market_data_access_token.

Neither token is minted when the stored one still works. The interactive token is checked with GET /user/balance, the funds endpoint. It is not /user/profile, because Wisdom Capital allows only about one profile call a day, which is a budget an object that is constructed several times a day cannot live inside. The funds endpoint authenticates identically, so it proves the same thing about the token without spending that allowance. The market data token is checked with GET /apimarketdata/config/clientConfig in the same spirit.

Wisdom Capital rate limits on a rolling window regardless of endpoint, and a rate-limit rejection (HTTP 429, or an error code containing e-apirl) is not a reason to log in again. The rate limiter runs only after the request has authenticated, and its error names the authenticated user, so a throttled check still proves the token is good. An expired or missing token instead comes back as e-token-0002 ("Please Provide token to Authenticate"), which does mean a fresh login is needed. A previously failed login stored the string "None" rather than a token, which also means a fresh login.

A market data login that fails is recorded in market_data_session_error rather than raised, because the scripts that poll orders, positions and funds need only the interactive token and must not be stopped by a market data credential they never use. bin/wisdom_capital/session/connect reports that error and exits non-zero, so the morning login is still where a broken market data credential is caught.

Raises:

Type Description
WisdomCapitalAPIException

The legacy login found no access token on the login page, the host lookup returned no unique key, or the interactive session request returned no token.

Example programs

examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_1_reading_the_balance.py
"""Reads the account balance from Wisdom Capital's REST API with a stored access token.

`WisdomCapitalAPI` is the object every Wisdom Capital script talks to the broker through. Its `get`, `post`, `put`, `patch` and `delete` methods all go through the broker's own `_request`, which adds the credentials of the login in force, sends the request, and wraps a successful answer in a dictionary with `status`, `code`, `data` and a `timestamp`.

The real constructor reads the broker's settings from MongoDB, probes `GET /interactive/user/balance` and the market data application's `clientConfig`, and logs in to Wisdom Capital with a session request for the interactive application and a second login for the market data application when the stored token no longer works. This program must never log in, so a small subclass, `WisdomCapitalAPIWithoutLogin`, skips that constructor and is handed the settings and a stand-in for Redis that already holds the stored login. `requests.request` is replaced, only while each request is sent, by a stand-in server that records the request and answers with a recorded-looking Wisdom Capital response, so nothing reaches the network.

Notice the headers the request carried, which were built from the stored login and, where the broker needs them, from the settings, and that `data` holds only the `result` object, because `_request` unwraps the envelope Symphony XTS puts around every answer, and that the request went out with certificate checking switched off (`verify=False`), because Wisdom Capital's host serves a certificate issued to another domain. The `timestamp` is left out of the output because it is the moment the program ran.

Run it from the project root:

    python examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_1_reading_the_balance.py
"""

import json
import logging
import unittest.mock

import requests

from stock_brokers.api.wisdom_capital import (
    WisdomCapitalAPI,
)

STORED_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'morning-token',
    'last_login': '2026-09-30 07:00:05',
}

RECORDED_ANSWER = {
    'type': 'success',
    'code': 's-user-0001',
    'description': 'Get Balance Success',
    'result': {
        'BalanceList': [
            {
                'limitHeader': 'ALL|ALL|ALL',
                'limitObject': {
                    'RMSSubLimits': {
                        'cashAvailable': '250000',
                        'netMarginAvailable': '248500.5',
                        'marginUtilized': '1499.5',
                    },
                },
            },
        ],
    },
}


class SharedLoginCache:
    """A dictionary-backed stand-in for Redis, holding the `last_login` hash that every process reads its token from.

    Attributes:
        hashes (dict): Each hash name mapped to a dictionary of its fields.
    """

    def __init__(self):
        """Starts with nothing stored.

        Returns:
            None: This method returns nothing.
        """
        self.hashes = {}

    def hget(self, name, key):
        """Reads one field of a hash.

        Args:
            name (str): The hash name.
            key (str): The field name.

        Returns:
            str | None: The stored value, or None when there is none.
        """
        fields = self.hashes.get(name, {})
        return fields.get(key)

    def hset(self, name, key, value):
        """Writes one field of a hash, as a login does.

        Args:
            name (str): The hash name.
            key (str): The field name.
            value (str): The value to store.

        Returns:
            int: Always 1, the number of fields Redis reports written.
        """
        if name not in self.hashes:
            self.hashes[name] = {}
        self.hashes[name][key] = value
        return 1


class RecordedBrokerServer:
    """A stand-in for `requests.request` that answers every call with one recorded Wisdom Capital response and remembers what was sent.

    Attributes:
        status_code (int): The HTTP status every answer carries.
        content_type (str): The Content-Type header every answer carries.
        body (str): The body every answer carries.
        calls (list): One dictionary per request, holding its method, URL, headers, body and certificate setting.
    """

    def __init__(self, status_code, content_type, body):
        """Holds the answer to give.

        Args:
            status_code (int): The HTTP status every answer carries.
            content_type (str): The Content-Type header every answer carries.
            body (str): The body every answer carries.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.content_type = content_type
        self.body = body
        self.calls = []

    def request(self, method, url, **options):
        """Records one request and answers it, in place of `requests.request`.

        Args:
            method (str): The HTTP method.
            url (str): The URL requested.
            **options (Any): The remaining `requests.request` arguments, such as `headers` and `data`.

        Returns:
            requests.Response: The recorded answer.
        """
        self.calls.append({
            'method': method,
            'url': url,
            'headers': options.get('headers'),
            'data': options.get('data'),
            'verify': options.get('verify'),
        })
        response = requests.Response()
        response.status_code = self.status_code
        response.headers['Content-Type'] = self.content_type
        response._content = self.body.encode('utf-8')
        response.url = url
        return response


class WisdomCapitalAPIWithoutLogin(WisdomCapitalAPI):
    """A `WisdomCapitalAPI` that is handed its settings and cache instead of loading them and logging in.

    The real constructor reads the `wisdom_capital` settings document from MongoDB and then logs in to Wisdom Capital if the stored token no longer works. This subclass sets the same attributes from what it is given and does nothing else, so every request it makes still runs `WisdomCapitalAPI`'s own `_request`.
    """

    def __init__(self, cache, settings, stored_login):
        """Sets the attributes the real constructor would have loaded.

        MongoDB is left out because the stored login is handed in here and is already in the cache, which is the first place `_current_login` looks.

        Args:
            cache (SharedLoginCache): The stand-in for Redis, already holding the stored login.
            settings (dict): The broker's settings document.
            stored_login (dict): The broker's `last_login` document, as the real constructor reads it from MongoDB.

        Returns:
            None: This method returns nothing.
        """
        self._broker_name = 'wisdom_capital'
        self._cache = cache
        self._mongo_db = None
        self._logger = logging.getLogger('wisdom_capital')
        self._settings = settings
        self._last_login = stored_login
        self.market_data_session_error = None


class ReadingTheBalanceExample:
    """Sends one authenticated request through `WisdomCapitalAPI` and prints what went out and what came back.

    Attributes:
        cache (SharedLoginCache): The stand-in for Redis, holding the stored login.
        server (RecordedBrokerServer): The stand-in for Wisdom Capital's API.
        api (WisdomCapitalAPIWithoutLogin): The API object being shown.
    """

    def __init__(self):
        """Stores the morning login and builds the API object and the stand-in server.

        Returns:
            None: This method returns nothing.
        """
        self.cache = SharedLoginCache()
        self.cache.hset('last_login', 'wisdom_capital', json.dumps(STORED_LOGIN))
        settings = {
            'ucc_code': 'WC0001',
        }
        self.server = RecordedBrokerServer(200, 'application/json', json.dumps(RECORDED_ANSWER))
        self.api = WisdomCapitalAPIWithoutLogin(self.cache, settings, STORED_LOGIN)

    def run(self):
        """Sends the request and prints the request and the answer.

        Returns:
            None: This method returns nothing.
        """
        with unittest.mock.patch('requests.request', self.server.request):
            answer = self.api.get(url='https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001')
        call = self.server.calls[0]
        print(f'Request: {call["method"]} {call["url"]}')
        print('Headers sent:')
        for name, value in call['headers'].items():
            print(f'    {name}: {value}')
        print(f'Certificate checked: {call["verify"]}')
        print(f'Status: {answer["status"]}')
        print(f'HTTP code: {answer["code"]}')
        print('Data:')
        print(json.dumps(answer['data'], indent=4))


if __name__ == '__main__':
    ReadingTheBalanceExample().run()

It prints:

Request: GET https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001
Headers sent:
    Content-Type: application/json
    authorization: morning-token
Certificate checked: False
Status: success
HTTP code: 200
Data:
{
    "BalanceList": [
        {
            "limitHeader": "ALL|ALL|ALL",
            "limitObject": {
                "RMSSubLimits": {
                    "cashAvailable": "250000",
                    "netMarginAvailable": "248500.5",
                    "marginUtilized": "1499.5"
                }
            }
        }
    ]
}
examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_2_using_a_newer_login.py
"""Picks up an access token another process obtained, on the very next request.

Every Wisdom Capital script builds its own `WisdomCapitalAPI`, and more than one of them may log in during a day. A login writes the new token to MongoDB and then to the Redis `last_login` hash, and `WisdomCapitalAPI`'s `_request` reads that hash again before every request, keeping whichever login is newer by its `last_login` time. So a token obtained by any process is used by every other one without restarting anything.

The program sends two requests through one API object. Between them it writes a newer login into the stand-in for Redis, as another process's login would.

The real constructor reads the broker's settings from MongoDB, probes `GET /interactive/user/balance` and the market data application's `clientConfig`, and logs in to Wisdom Capital with a session request for the interactive application and a second login for the market data application when the stored token no longer works. This program must never log in, so a small subclass, `WisdomCapitalAPIWithoutLogin`, skips that constructor and is handed the settings and a stand-in for Redis that already holds the stored login. `requests.request` is replaced, only while each request is sent, by a stand-in server that records the request and answers with a recorded-looking Wisdom Capital response, so nothing reaches the network.

Notice that the second request carries the newer token although the object was never rebuilt.

Run it from the project root:

    python examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_2_using_a_newer_login.py
"""

import json
import logging
import unittest.mock

import requests

from stock_brokers.api.wisdom_capital import (
    WisdomCapitalAPI,
)

STORED_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'morning-token',
    'last_login': '2026-09-30 07:00:05',
}

NEWER_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'noon-token',
    'last_login': '2026-09-30 12:30:41',
}

RECORDED_ANSWER = {
    'type': 'success',
    'code': 's-user-0001',
    'description': 'Get Balance Success',
    'result': {
        'BalanceList': [],
    },
}


class SharedLoginCache:
    """A dictionary-backed stand-in for Redis, holding the `last_login` hash that every process reads its token from.

    Attributes:
        hashes (dict): Each hash name mapped to a dictionary of its fields.
    """

    def __init__(self):
        """Starts with nothing stored.

        Returns:
            None: This method returns nothing.
        """
        self.hashes = {}

    def hget(self, name, key):
        """Reads one field of a hash.

        Args:
            name (str): The hash name.
            key (str): The field name.

        Returns:
            str | None: The stored value, or None when there is none.
        """
        fields = self.hashes.get(name, {})
        return fields.get(key)

    def hset(self, name, key, value):
        """Writes one field of a hash, as a login does.

        Args:
            name (str): The hash name.
            key (str): The field name.
            value (str): The value to store.

        Returns:
            int: Always 1, the number of fields Redis reports written.
        """
        if name not in self.hashes:
            self.hashes[name] = {}
        self.hashes[name][key] = value
        return 1


class RecordedBrokerServer:
    """A stand-in for `requests.request` that answers every call with one recorded Wisdom Capital response and remembers what was sent.

    Attributes:
        status_code (int): The HTTP status every answer carries.
        content_type (str): The Content-Type header every answer carries.
        body (str): The body every answer carries.
        calls (list): One dictionary per request, holding its method, URL, headers, body and certificate setting.
    """

    def __init__(self, status_code, content_type, body):
        """Holds the answer to give.

        Args:
            status_code (int): The HTTP status every answer carries.
            content_type (str): The Content-Type header every answer carries.
            body (str): The body every answer carries.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.content_type = content_type
        self.body = body
        self.calls = []

    def request(self, method, url, **options):
        """Records one request and answers it, in place of `requests.request`.

        Args:
            method (str): The HTTP method.
            url (str): The URL requested.
            **options (Any): The remaining `requests.request` arguments, such as `headers` and `data`.

        Returns:
            requests.Response: The recorded answer.
        """
        self.calls.append({
            'method': method,
            'url': url,
            'headers': options.get('headers'),
            'data': options.get('data'),
            'verify': options.get('verify'),
        })
        response = requests.Response()
        response.status_code = self.status_code
        response.headers['Content-Type'] = self.content_type
        response._content = self.body.encode('utf-8')
        response.url = url
        return response


class WisdomCapitalAPIWithoutLogin(WisdomCapitalAPI):
    """A `WisdomCapitalAPI` that is handed its settings and cache instead of loading them and logging in.

    The real constructor reads the `wisdom_capital` settings document from MongoDB and then logs in to Wisdom Capital if the stored token no longer works. This subclass sets the same attributes from what it is given and does nothing else, so every request it makes still runs `WisdomCapitalAPI`'s own `_request`.
    """

    def __init__(self, cache, settings, stored_login):
        """Sets the attributes the real constructor would have loaded.

        MongoDB is left out because the stored login is handed in here and is already in the cache, which is the first place `_current_login` looks.

        Args:
            cache (SharedLoginCache): The stand-in for Redis, already holding the stored login.
            settings (dict): The broker's settings document.
            stored_login (dict): The broker's `last_login` document, as the real constructor reads it from MongoDB.

        Returns:
            None: This method returns nothing.
        """
        self._broker_name = 'wisdom_capital'
        self._cache = cache
        self._mongo_db = None
        self._logger = logging.getLogger('wisdom_capital')
        self._settings = settings
        self._last_login = stored_login
        self.market_data_session_error = None


class UsingANewerLoginExample:
    """Sends a request, lets another process log in, and sends another through the same object.

    Attributes:
        cache (SharedLoginCache): The stand-in for Redis, holding the shared login.
        server (RecordedBrokerServer): The stand-in for Wisdom Capital's API.
        api (WisdomCapitalAPIWithoutLogin): The API object being shown.
    """

    def __init__(self):
        """Stores the morning login and builds the API object and the stand-in server.

        Returns:
            None: This method returns nothing.
        """
        self.cache = SharedLoginCache()
        self.cache.hset('last_login', 'wisdom_capital', json.dumps(STORED_LOGIN))
        settings = {
            'ucc_code': 'WC0001',
        }
        self.server = RecordedBrokerServer(200, 'application/json', json.dumps(RECORDED_ANSWER))
        self.api = WisdomCapitalAPIWithoutLogin(self.cache, settings, STORED_LOGIN)

    def credential(self, call):
        """The credential one recorded request carried.

        Args:
            call (dict): One request recorded by the stand-in server.

        Returns:
            str: The `authorization` header.
        """
        return call['headers']['authorization']

    def run(self):
        """Sends a request, writes a newer login, sends another request and prints what each carried.

        Returns:
            None: This method returns nothing.
        """
        with unittest.mock.patch('requests.request', self.server.request):
            self.api.get(url='https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001')
        print(f'First request carried: {self.credential(self.server.calls[0])}')
        self.cache.hset('last_login', 'wisdom_capital', json.dumps(NEWER_LOGIN))
        print('Another process logged in again at 12:30:41 and wrote its login to Redis.')
        with unittest.mock.patch('requests.request', self.server.request):
            answer = self.api.get(url='https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001')
        print(f'Second request carried: {self.credential(self.server.calls[1])}')
        print(f'Request: {self.server.calls[1]["method"]} {self.server.calls[1]["url"]}')
        print(f'Status: {answer["status"]}, HTTP code: {answer["code"]}')
        print('Data:')
        print(json.dumps(answer['data'], indent=4))


if __name__ == '__main__':
    UsingANewerLoginExample().run()

It prints:

First request carried: morning-token
Another process logged in again at 12:30:41 and wrote its login to Redis.
Second request carried: noon-token
Request: GET https://trade.wisdomcapital.in/interactive/user/balance?clientID=WC0001
Status: success, HTTP code: 200
Data:
{
    "BalanceList": []
}
examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_3_telling_rate_limits_from_refused_sessions.py
"""Sorts Wisdom Capital's refusals into rate limits, refused sessions and everything else.

Wisdom Capital's platform, Symphony XTS, rate limits every request on a rolling window, and it checks the token before it checks the rate. So a request refused for rate still proves the token is good, while one refused for its token means a fresh login is needed. `WisdomCapitalAPI.is_rate_limited` and `WisdomCapitalAPI.is_session_refused` make that call from whatever a request raised, by its code (429 or 401) or by the XTS error codes in its text (`e-apirl` for rate, `e-session` and `e-token` for the session).

The constructor uses them to decide whether to log in again: it keeps the stored token after a rate limit, logs in after a refused session, and keeps the token when the error says nothing either way, such as a gateway error or a dropped connection, because a new market data login would invalidate the token every other process is using.

Both are static methods and need no logged-in object, so this program builds only exceptions, of the kinds the platform and the network produce. It reaches no broker.

Notice that the gateway error and the dropped connection are neither, which is what keeps a passing outage from triggering a login.

Run it from the project root:

    python examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_3_telling_rate_limits_from_refused_sessions.py
"""

import requests

from stock_brokers.api.wisdom_capital import (
    WisdomCapitalAPI,
    WisdomCapitalAPIException,
)


class TellingRefusalsApartExample:
    """Classifies a list of errors and says what the constructor would do after each.

    Attributes:
        errors (list): Pairs of (description, exception) to classify.
    """

    def __init__(self):
        """Builds the errors to classify.

        Returns:
            None: This method returns nothing.
        """
        self.errors = [
            (
                'HTTP 429 from the gateway',
                WisdomCapitalAPIException(code=429, message='Too Many Requests'),
            ),
            (
                'XTS rate limit code',
                WisdomCapitalAPIException(code='e-apirl-0004', message='Rate limit exceeded for user WC0001'),
            ),
            (
                'XTS missing or expired token',
                WisdomCapitalAPIException(code='e-token-0002', message='Please Provide token to Authenticate'),
            ),
            (
                'HTTP 401',
                WisdomCapitalAPIException(code=401, message='Unauthorized'),
            ),
            (
                'HTTP 502 gateway page',
                WisdomCapitalAPIException(code=502, message='<html><body><h1>502 Bad Gateway</h1></body></html>'),
            ),
            (
                'dropped connection',
                requests.ConnectionError('Connection aborted.'),
            ),
        ]

    def run(self):
        """Prints each error's classification and the resulting decision.

        Returns:
            None: This method returns nothing.
        """
        for description, error in self.errors:
            rate_limited = WisdomCapitalAPI.is_rate_limited(error)
            session_refused = WisdomCapitalAPI.is_session_refused(error)
            if rate_limited:
                decision = 'keep the token, it authenticated'
            elif session_refused:
                decision = 'log in again'
            else:
                decision = 'keep the token, the error says nothing about it'
            print(f'{description}:')
            print(f'    rate limited={rate_limited} session refused={session_refused} -> {decision}')


if __name__ == '__main__':
    TellingRefusalsApartExample().run()

It prints:

HTTP 429 from the gateway:
    rate limited=True session refused=False -> keep the token, it authenticated
XTS rate limit code:
    rate limited=True session refused=False -> keep the token, it authenticated
XTS missing or expired token:
    rate limited=False session refused=True -> log in again
HTTP 401:
    rate limited=False session refused=True -> log in again
HTTP 502 gateway page:
    rate limited=False session refused=False -> keep the token, the error says nothing about it
dropped connection:
    rate limited=False session refused=False -> keep the token, the error says nothing about it
examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_4_replacing_a_refused_market_data_session.py
"""Replaces a refused market data session, reusing one another process already obtained where it can.

Wisdom Capital's platform issues exactly one market data session per application key, and a second login invalidates the first token. So when a process finds its market data token refused, it calls `replace_market_data_session` with that stale token. If another process has already published a different token in the shared login, that token is returned and nobody logs in. Otherwise the process takes a Redis lock, checks once more, and logs in itself, releasing the lock afterwards whatever happens. `market_data_session` reads the market data token and user id in force from the shared login.

The program shows both paths. In the first, another process has already replaced the token, so the call returns it without taking the lock. In the second, nothing has replaced it, so the lock is taken and a login is attempted; the settings here deliberately lack the market data key pair, so the login stops with a `WisdomCapitalAPIException` before sending anything, and the lock is released.

The real constructor reads the settings from MongoDB and logs in to both of Wisdom Capital's applications when their stored tokens no longer work. This program must never log in, so a small subclass, `WisdomCapitalAPIWithoutLogin`, skips that constructor and is handed the settings and a stand-in for Redis that holds the shared login and the lock. No request reaches the network.

Notice that the first path takes no lock, and that the second path leaves no lock behind although the login failed.

Run it from the project root:

    python examples/stock_brokers/api/wisdom_capital/WisdomCapitalAPI/example_4_replacing_a_refused_market_data_session.py
"""

import json
import logging

from stock_brokers.api.wisdom_capital import (
    MARKET_DATA_LOCK_KEY,
    WisdomCapitalAPI,
    WisdomCapitalAPIException,
)

MORNING_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'interactive-token',
    'last_login': '2026-09-30 07:00:05',
    'market_data_access_token': 'market-data-token-morning',
    'market_data_user_id': 'WC0001',
}

REPLACED_LOGIN = {
    'broker_name': 'wisdom_capital',
    'access_token': 'interactive-token',
    'last_login': '2026-09-30 07:00:05',
    'market_data_access_token': 'market-data-token-noon',
    'market_data_user_id': 'WC0001',
}


class SharedLoginCache:
    """A dictionary-backed stand-in for Redis, holding the shared login and the market data lock.

    Attributes:
        hashes (dict): Each hash name mapped to a dictionary of its fields.
        keys (dict): Each plain key mapped to its value.
        locks_taken (list): Every key set with `nx=True` that was not already present.
    """

    def __init__(self):
        """Starts with nothing stored.

        Returns:
            None: This method returns nothing.
        """
        self.hashes = {}
        self.keys = {}
        self.locks_taken = []

    def hget(self, name, key):
        """Reads one field of a hash.

        Args:
            name (str): The hash name.
            key (str): The field name.

        Returns:
            str | None: The stored value, or None when there is none.
        """
        fields = self.hashes.get(name, {})
        return fields.get(key)

    def hset(self, name, key, value):
        """Writes one field of a hash, as a login does.

        Args:
            name (str): The hash name.
            key (str): The field name.
            value (str): The value to store.

        Returns:
            int: Always 1, the number of fields Redis reports written.
        """
        if name not in self.hashes:
            self.hashes[name] = {}
        self.hashes[name][key] = value
        return 1

    def set(self, name, value, nx=False, ex=None):
        """Sets a plain key, only when it is absent if `nx` is given, as the lock is taken.

        Args:
            name (str): The key.
            value (str): The value to store.
            nx (bool): True to set the key only when it does not exist.
            ex (int | None): The expiry in seconds, which the stand-in ignores.

        Returns:
            bool: True when the key was set.
        """
        if nx and name in self.keys:
            return False
        self.keys[name] = value
        if nx:
            self.locks_taken.append(name)
        return True

    def delete(self, name):
        """Removes a plain key, as the lock is released.

        Args:
            name (str): The key.

        Returns:
            int: 1 when the key existed, otherwise 0.
        """
        if name in self.keys:
            del self.keys[name]
            return 1
        return 0


class WisdomCapitalAPIWithoutLogin(WisdomCapitalAPI):
    """A `WisdomCapitalAPI` that is handed its settings and cache instead of loading them and logging in.

    The real constructor reads the `wisdom_capital` settings document from MongoDB and then establishes both of the platform's sessions. This subclass sets the same attributes from what it is given and does nothing else.
    """

    def __init__(self, cache, settings, stored_login):
        """Sets the attributes the real constructor would have loaded.

        Args:
            cache (SharedLoginCache): The stand-in for Redis, already holding the shared login.
            settings (dict): The broker's settings document.
            stored_login (dict): The broker's `last_login` document, as the real constructor reads it from MongoDB.

        Returns:
            None: This method returns nothing.
        """
        self._broker_name = 'wisdom_capital'
        self._cache = cache
        self._mongo_db = None
        self._logger = logging.getLogger('wisdom_capital')
        self._settings = settings
        self._last_login = stored_login
        self.market_data_session_error = None


class ReplacingAMarketDataSessionExample:
    """Replaces a refused market data token once when another process already has, and once when nobody has.

    Attributes:
        settings (dict): The broker's settings document, without the market data key pair.
    """

    def __init__(self):
        """Holds the settings both paths use.

        Returns:
            None: This method returns nothing.
        """
        self.settings = {
            'ucc_code': 'WC0001',
        }

    def build_api(self, shared_login):
        """Builds an API object whose shared login is the one given.

        Args:
            shared_login (dict): The login document another process last wrote to Redis.

        Returns:
            tuple: (SharedLoginCache, WisdomCapitalAPIWithoutLogin), the stand-in cache and the API object.
        """
        cache = SharedLoginCache()
        cache.hset('last_login', 'wisdom_capital', json.dumps(shared_login))
        api = WisdomCapitalAPIWithoutLogin(cache, self.settings, MORNING_LOGIN)
        return cache, api

    def run(self):
        """Runs both paths and prints what each returned or raised.

        Returns:
            None: This method returns nothing.
        """
        print('Path 1: another process has already replaced the refused token.')
        cache, api = self.build_api(MORNING_LOGIN)
        print(f'    Session in force: {api.market_data_session()}')
        cache.hset('last_login', 'wisdom_capital', json.dumps(REPLACED_LOGIN))
        session = api.replace_market_data_session(stale_access_token='market-data-token-morning')
        print(f'    Replacement returned: {session}')
        print(f'    Locks taken: {cache.locks_taken}')

        print('Path 2: nobody has replaced it, and the settings lack the market data key pair.')
        cache, api = self.build_api(MORNING_LOGIN)
        try:
            api.replace_market_data_session(stale_access_token='market-data-token-morning')
        except WisdomCapitalAPIException as error:
            print(f'    Raised: {type(error).__name__} code={error.code}')
            print(f'    message: {error.message}')
        print(f'    Locks taken: {cache.locks_taken}')
        print(f'    Lock still held: {MARKET_DATA_LOCK_KEY in cache.keys}')


if __name__ == '__main__':
    ReplacingAMarketDataSessionExample().run()

It prints:

Path 1: another process has already replaced the refused token.
    Session in force: {'access_token': 'market-data-token-morning', 'user_id': 'WC0001'}
    Replacement returned: {'access_token': 'market-data-token-noon', 'user_id': 'WC0001'}
    Locks taken: []
Path 2: nobody has replaced it, and the settings lack the market data key pair.
    Raised: WisdomCapitalAPIException code=500
    message: wisdom_capital has no price_api_key or price_api_secret in its settings, so the market data session cannot be established.
    Locks taken: ['wisdom_capital:session:marketdata:lock']
    Lock still held: False

Methods:

Name Description
is_rate_limited

Whether Wisdom Capital refused a request for rate rather than for the token it carried.

is_session_refused

Whether Wisdom Capital refused a request because the token it carried is expired, unknown or missing.

market_data_session

The market data token and user id in force now, read from the shared login rather than from this object.

replace_market_data_session

Establishes a market data session, unless another process has already replaced the one that failed.

get

HTTP GET API call to Broker's API server.

post

HTTP POST API call to Broker's API server.

put

HTTP PUT API call to Broker's API server.

patch

HTTP PATCH API call to Broker's API server.

delete

HTTP DELETE API call to Broker's API server.

Source code in stock_brokers/api/wisdom_capital.py
def __init__(self):
    """Establishes both of the sessions Wisdom Capital's platform needs, logging in only where the stored token has stopped working.

    Symphony XTS splits a broker into two applications with separate credentials. The interactive application places orders and reports the account, and the market data application serves quotes and charts; each issues its own token and refuses the other's. Constructing this class establishes both, and leaves them in the `last_login` document as `access_token` and `market_data_access_token`.

    Neither token is minted when the stored one still works. The interactive token is checked with `GET /user/balance`, the funds endpoint. It is not `/user/profile`, because Wisdom Capital allows only about one profile call a day, which is a budget an object that is constructed several times a day cannot live inside. The funds endpoint authenticates identically, so it proves the same thing about the token without spending that allowance. The market data token is checked with `GET /apimarketdata/config/clientConfig` in the same spirit.

    Wisdom Capital rate limits on a rolling window regardless of endpoint, and a rate-limit rejection (HTTP 429, or an error code containing `e-apirl`) is not a reason to log in again. The rate limiter runs only after the request has authenticated, and its error names the authenticated user, so a throttled check still proves the token is good. An expired or missing token instead comes back as `e-token-0002` ("Please Provide token to Authenticate"), which does mean a fresh login is needed. A previously failed login stored the string "None" rather than a token, which also means a fresh login.

    A market data login that fails is recorded in `market_data_session_error` rather than raised, because the scripts that poll orders, positions and funds need only the interactive token and must not be stopped by a market data credential they never use. `bin/wisdom_capital/session/connect` reports that error and exits non-zero, so the morning login is still where a broken market data credential is caught.

    Raises:
        WisdomCapitalAPIException: The legacy login found no access token on the login page, the host lookup returned no unique key, or the interactive session request returned no token.
    """
    super().__init__(broker_name="wisdom_capital")

    self.market_data_session_error = None
    self._ensure_trading_session()
    try:
        self._ensure_market_data_session()
    except Exception as exception:
        self.market_data_session_error = exception
        self._logger.warning("Wisdom Capital's market data session could not be established (%s: %s). The interactive session is unaffected.", type(exception).__name__, exception)

is_rate_limited staticmethod

is_rate_limited(exception)

Whether Wisdom Capital refused a request for rate rather than for the token it carried.

Parameters:

Name Type Description Default
exception Exception

Whatever the request raised.

required

Returns:

Type Description
bool

True when the refusal came from the rate limiter, which runs only after the request has authenticated and so still proves the token is good.

Example usage

From example_3_telling_rate_limits_from_refused_sessions.py
rate_limited = WisdomCapitalAPI.is_rate_limited(error)
Source code in stock_brokers/api/wisdom_capital.py
@staticmethod
def is_rate_limited(exception):
    """Whether Wisdom Capital refused a request for rate rather than for the token it carried.

    Args:
        exception (Exception): Whatever the request raised.

    Returns:
        bool: True when the refusal came from the rate limiter, which runs only after the request has authenticated and so still proves the token is good.
    """
    if getattr(exception, "code", None) in (429, "429"):
        return True
    text = str(exception).lower()
    for marker in RATE_LIMIT_MARKERS:
        if marker in text:
            return True
    return False

is_session_refused staticmethod

is_session_refused(exception)

Whether Wisdom Capital refused a request because the token it carried is expired, unknown or missing.

Parameters:

Name Type Description Default
exception Exception

Whatever the request raised.

required

Returns:

Type Description
bool

True when the platform named the session or the token as the problem.

Example usage

From example_3_telling_rate_limits_from_refused_sessions.py
session_refused = WisdomCapitalAPI.is_session_refused(error)
Source code in stock_brokers/api/wisdom_capital.py
@staticmethod
def is_session_refused(exception):
    """Whether Wisdom Capital refused a request because the token it carried is expired, unknown or missing.

    Args:
        exception (Exception): Whatever the request raised.

    Returns:
        bool: True when the platform named the session or the token as the problem.
    """
    if getattr(exception, "code", None) in (401, "401"):
        return True
    text = str(exception).lower()
    for marker in SESSION_REFUSED_MARKERS:
        if marker in text:
            return True
    return False

market_data_session

market_data_session()

The market data token and user id in force now, read from the shared login rather than from this object.

Returns:

Type Description
dict

access_token (str | None) and user_id (str | None), both None when no market data session is stored.

Example usage

From example_4_replacing_a_refused_market_data_session.py
print(f'    Session in force: {api.market_data_session()}')
Source code in stock_brokers/api/wisdom_capital.py
def market_data_session(self):
    """The market data token and user id in force now, read from the shared login rather than from this object.

    Returns:
        dict: `access_token` (str | None) and `user_id` (str | None), both None when no market data session is stored.
    """
    login = self._current_login() or {}
    return {
        "access_token": login.get("market_data_access_token"),
        "user_id": login.get("market_data_user_id"),
    }

replace_market_data_session

replace_market_data_session(stale_access_token=None)

Establishes a market data session, unless another process has already replaced the one that failed.

XTS issues exactly one market data session per application key, and a second login invalidates the first token rather than handing out a second. Two processes that each log in therefore take turns invalidating each other's session, so whoever finds the stored token unusable takes a Redis lock, logs in once, and publishes the result for everyone else to read.

Parameters:

Name Type Description Default
stale_access_token str | None

The market data token that was just refused, so that a token another process has already published in its place is recognised as a replacement rather than as the same failure.

None

Returns:

Type Description
dict

access_token (str) and user_id (str | None) of the session now in force.

Raises:

Type Description
WisdomCapitalAPIException

The market data credentials are missing, the login returned no token, or another process held the lock for longer than MARKET_DATA_WAIT_SECONDS.

Example usage

From example_4_replacing_a_refused_market_data_session.py
session = api.replace_market_data_session(stale_access_token='market-data-token-morning')
From example_4_replacing_a_refused_market_data_session.py
api.replace_market_data_session(stale_access_token='market-data-token-morning')
Source code in stock_brokers/api/wisdom_capital.py
def replace_market_data_session(self, stale_access_token=None):
    """Establishes a market data session, unless another process has already replaced the one that failed.

    XTS issues exactly one market data session per application key, and a second login invalidates the first token rather than handing out a second. Two processes that each log in therefore take turns invalidating each other's session, so whoever finds the stored token unusable takes a Redis lock, logs in once, and publishes the result for everyone else to read.

    Args:
        stale_access_token (str | None): The market data token that was just refused, so that a token another process has already published in its place is recognised as a replacement rather than as the same failure.

    Returns:
        dict: `access_token` (str) and `user_id` (str | None) of the session now in force.

    Raises:
        WisdomCapitalAPIException: The market data credentials are missing, the login returned no token, or another process held the lock for longer than `MARKET_DATA_WAIT_SECONDS`.
    """
    published = self._published_market_data_session(stale_access_token)
    if published["access_token"]:
        return published

    deadline = time.time() + MARKET_DATA_WAIT_SECONDS
    while not self._cache.set(MARKET_DATA_LOCK_KEY, str(os.getpid()), nx=True, ex=MARKET_DATA_LOCK_SECONDS):
        time.sleep(1)
        published = self._published_market_data_session(stale_access_token)
        if published["access_token"]:
            return published
        if time.time() > deadline:
            raise WisdomCapitalAPIException(
                code="500",
                message="Timed out waiting for another process to log in to Wisdom Capital's market data API.",
            )

    try:
        published = self._published_market_data_session(stale_access_token)
        if published["access_token"]:
            return published
        return self._log_in_to_market_data()
    finally:
        self._cache.delete(MARKET_DATA_LOCK_KEY)

_ensure_trading_session

_ensure_trading_session()

Makes sure the interactive token in the shared login is one Wisdom Capital still accepts, logging in again when it is not.

Returns:

Type Description
None

The token in force is left in self._last_login.

Raises:

Type Description
WisdomCapitalAPIException

The login produced no token.

Source code in stock_brokers/api/wisdom_capital.py
def _ensure_trading_session(self):
    """Makes sure the interactive token in the shared login is one Wisdom Capital still accepts, logging in again when it is not.

    Returns:
        None: The token in force is left in `self._last_login`.

    Raises:
        WisdomCapitalAPIException: The login produced no token.
    """
    stored_token = (self._current_login() or {}).get("access_token")
    if stored_token and stored_token != "None" and self._trading_token_works():
        return
    self._log_in_to_trading()

_trading_token_works

_trading_token_works()

Asks Wisdom Capital for the account balance to find out whether the stored interactive token is still accepted.

Returns:

Type Description
bool

True when the balance came back, or when the request was refused for rate, which happens only after it has authenticated.

Source code in stock_brokers/api/wisdom_capital.py
def _trading_token_works(self):
    """Asks Wisdom Capital for the account balance to find out whether the stored interactive token is still accepted.

    Returns:
        bool: True when the balance came back, or when the request was refused for rate, which happens only after it has authenticated.
    """
    try:
        self.get(url=f"{INTERACTIVE_BALANCE_URL}?clientID={self._settings['ucc_code']}")
    except Exception as exception:
        return self.is_rate_limited(exception)
    return True

_log_in_to_trading

_log_in_to_trading()

Exchanges the interactive credentials for a token and stores it.

Returns:

Type Description
dict

The stored login after the token was written into it.

Raises:

Type Description
WisdomCapitalAPIException

The legacy login found no access token on the login page, the host lookup returned no unique key, or the session request returned no token.

Source code in stock_brokers/api/wisdom_capital.py
def _log_in_to_trading(self):
    """Exchanges the interactive credentials for a token and stores it.

    Returns:
        dict: The stored login after the token was written into it.

    Raises:
        WisdomCapitalAPIException: The legacy login found no access token on the login page, the host lookup returned no unique key, or the session request returned no token.
    """
    session_request = {
        "appKey": self._settings['api_key'],
        "secretKey": self._settings['api_secret'],
    }

    if self._USE_LEGACY_LOGIN:
        session_request.update(self._browser_login_fields())

    response = requests.post(
        INTERACTIVE_SESSION_URL,
        headers={'Content-Type': 'application/json'},
        json=session_request,
        verify=self._VERIFY_SSL
    )
    login_data = response.json()
    if "result" not in login_data or "token" not in (login_data.get("result") or {}):
        raise WisdomCapitalAPIException(
            code=response.status_code,
            message=f"Wisdom Capital session login did not return a token: {login_data}",
        )

    self._logger.info("Logged in to Wisdom Capital's interactive API.")
    return self._store_login({
        "access_token": login_data['result']['token'],
        "last_login": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    })

_browser_login_fields

_browser_login_fields()

Drives Wisdom Capital's web login page with Selenium and returns the extra fields the legacy session request carries.

Returns:

Type Description
dict

accessToken, scraped from the page the login lands on, and uniqueKey, from Symphony's host lookup.

Raises:

Type Description
WisdomCapitalAPIException

No access token could be extracted from the login page, or the host lookup returned no unique key.

Source code in stock_brokers/api/wisdom_capital.py
def _browser_login_fields(self):
    """Drives Wisdom Capital's web login page with Selenium and returns the extra fields the legacy session request carries.

    Returns:
        dict: `accessToken`, scraped from the page the login lands on, and `uniqueKey`, from Symphony's host lookup.

    Raises:
        WisdomCapitalAPIException: No access token could be extracted from the login page, or the host lookup returned no unique key.
    """
    chrome_options = Options()
    chrome_options.add_argument('--headless')
    chrome_options.add_argument('--no-sandbox')
    chrome_options.add_argument('--disable-dev-shm-usage')
    chrome_options.add_argument('--ignore-certificate-errors')
    driver = Chrome(options=chrome_options)
    login_url = f'https://trade.wisdomcapital.in/interactive/thirdparty?appKey={self._settings["api_key"]}&returnURL=https://trade.wisdomcapital.in/interactive/testapi#!/logIn'
    driver.get(login_url)
    time.sleep(2)

    user_id = driver.find_element(By.XPATH, '//*[@id="loginPart"]/div/div/div[2]/div[2]/form/div/input')
    user_id.send_keys(self._settings['ucc_code'])
    btn_validate = driver.find_element(By.XPATH, '//*[@id="loginPart"]/div/div/div[2]/div[2]/form/button')
    btn_validate.click()
    time.sleep(2)

    password = driver.find_element(By.XPATH, '//*[@id="login_password_field"]')
    password.send_keys(f'{self._settings["password"]}')
    chk_confirm = driver.find_element(By.XPATH, '//*[@id="confirmimage"]')
    chk_confirm.click()
    btn_submit = driver.find_element(By.XPATH, '//*[@id="loginPart"]/div/div/div/div[2]/form/div[4]/div[2]/button')
    btn_submit.click()
    time.sleep(2)

    pin = driver.find_element(By.XPATH, '//*[@id="efirstPin"]')
    pin.send_keys(f'{self._settings["pin"]}')
    btn_submit = driver.find_element(By.XPATH, '//*[@id="loginPart"]/div/div/div/div[2]/form/div[2]/button')
    btn_submit.click()
    time.sleep(2)

    try:
        soup = BeautifulSoup(driver.page_source, 'html.parser')
        pre_tag = soup.find('pre')
        if not pre_tag:
            raise ValueError("Could not find the <pre> tag in the HTML.")
        outer_data = json.loads(pre_tag.get_text().strip())
        session_str = outer_data.get("session")
        if not session_str:
            raise KeyError("Key 'session' not found in the JSON.")
        inner_data = json.loads(session_str)
        access_token = inner_data.get("accessToken")
        if not access_token:
            raise KeyError("Key 'accessToken' not found in the nested session JSON.")
    except (json.JSONDecodeError, TypeError, ValueError, KeyError) as error:
        self._logger.error("Could not parse the Wisdom Capital login page: %s", error)
        access_token = None

    driver.quit()

    if not access_token:
        raise WisdomCapitalAPIException(
            code="500",
            message="Could not extract an access token from the Wisdom Capital login page. The credentials/PIN may have been rejected or the login page changed.",
        )

    response = requests.post(HOST_LOOKUP_URL, headers={'Content-Type': 'application/json'}, data=json.dumps({"accesspassword": "2021HostLookUpAccess", "version": "interactive_1.0.2"}))
    hostlookup_data = response.json()
    if "result" not in hostlookup_data or "uniqueKey" not in (hostlookup_data.get("result") or {}):
        raise WisdomCapitalAPIException(
            code=response.status_code,
            message=f"Wisdom Capital hostlookup did not return a uniqueKey: {hostlookup_data}",
        )

    return {
        "uniqueKey": hostlookup_data['result']['uniqueKey'],
        "accessToken": access_token,
    }

_ensure_market_data_session

_ensure_market_data_session()

Makes sure the market data token in the shared login is one Wisdom Capital still accepts, logging in again when it is not.

Returns:

Type Description
None

The token in force is left in self._last_login.

Raises:

Type Description
WisdomCapitalAPIException

No market data session could be established.

Source code in stock_brokers/api/wisdom_capital.py
def _ensure_market_data_session(self):
    """Makes sure the market data token in the shared login is one Wisdom Capital still accepts, logging in again when it is not.

    Returns:
        None: The token in force is left in `self._last_login`.

    Raises:
        WisdomCapitalAPIException: No market data session could be established.
    """
    stored_token = (self._current_login() or {}).get("market_data_access_token")
    if stored_token and stored_token != "None" and self._market_data_token_works(stored_token):
        return
    self.replace_market_data_session(stale_access_token=stored_token)

_market_data_token_works

_market_data_token_works(access_token)

Asks the market data application for its client configuration to find out whether a token is still accepted.

A failure that does not name the session is deliberately read as "cannot tell" and the token is kept. Minting a new one invalidates whatever the live quotes feed and the candle downloader are holding, which is too destructive to do on the strength of a timeout or a bad gateway.

Parameters:

Name Type Description Default
access_token str

The market data token to check.

required

Returns:

Type Description
bool

True when the token is still accepted, or when the answer said nothing about the session.

Source code in stock_brokers/api/wisdom_capital.py
def _market_data_token_works(self, access_token):
    """Asks the market data application for its client configuration to find out whether a token is still accepted.

    A failure that does not name the session is deliberately read as "cannot tell" and the token is kept. Minting a new one invalidates whatever the live quotes feed and the candle downloader are holding, which is too destructive to do on the strength of a timeout or a bad gateway.

    Args:
        access_token (str): The market data token to check.

    Returns:
        bool: True when the token is still accepted, or when the answer said nothing about the session.
    """
    try:
        self.get(url=MARKET_DATA_PROBE_URL, headers=self._market_data_headers(access_token))
    except Exception as exception:
        if self.is_rate_limited(exception):
            return True
        if self.is_session_refused(exception):
            return False
        self._logger.warning("Wisdom Capital's market data check failed for a reason that is not the token (%s: %s). Keeping the stored market data token.", type(exception).__name__, exception)
        return True
    return True

_market_data_headers

_market_data_headers(access_token)

The headers a market data request carries, which authorize with the market data token rather than the interactive one.

Parameters:

Name Type Description Default
access_token str

The market data token.

required

Returns:

Type Description
dict

The headers for a market data request.

Source code in stock_brokers/api/wisdom_capital.py
def _market_data_headers(self, access_token):
    """The headers a market data request carries, which authorize with the market data token rather than the interactive one.

    Args:
        access_token (str): The market data token.

    Returns:
        dict: The headers for a market data request.
    """
    return {
        'Content-Type': 'application/json',
        'authorization': access_token,
    }

_published_market_data_session

_published_market_data_session(stale_access_token)

The market data session another process has published, when it is not the one that has just failed.

Parameters:

Name Type Description Default
stale_access_token str | None

The market data token that was refused, or None when there was none to refuse.

required

Returns:

Type Description
dict

access_token (str | None) and user_id (str | None), with access_token None when nothing usable is published.

Source code in stock_brokers/api/wisdom_capital.py
def _published_market_data_session(self, stale_access_token):
    """The market data session another process has published, when it is not the one that has just failed.

    Args:
        stale_access_token (str | None): The market data token that was refused, or None when there was none to refuse.

    Returns:
        dict: `access_token` (str | None) and `user_id` (str | None), with `access_token` None when nothing usable is published.
    """
    login = self._current_login() or {}
    access_token = login.get("market_data_access_token")
    if not access_token or access_token == "None" or access_token == stale_access_token:
        return {
            "access_token": None,
            "user_id": None,
        }
    return {
        "access_token": access_token,
        "user_id": login.get("market_data_user_id"),
    }

_log_in_to_market_data

_log_in_to_market_data()

Exchanges the market data credentials for a token and stores it.

Returns:

Type Description
dict

access_token (str) and user_id (str | None) of the session just established.

Raises:

Type Description
WisdomCapitalAPIException

The market data credentials are missing from the broker's settings, or the login returned no token.

Source code in stock_brokers/api/wisdom_capital.py
def _log_in_to_market_data(self):
    """Exchanges the market data credentials for a token and stores it.

    Returns:
        dict: `access_token` (str) and `user_id` (str | None) of the session just established.

    Raises:
        WisdomCapitalAPIException: The market data credentials are missing from the broker's settings, or the login returned no token.
    """
    settings = self._settings or {}
    application_key = settings.get("price_api_key")
    application_secret = settings.get("price_api_secret")
    if not application_key or not application_secret:
        raise WisdomCapitalAPIException(
            code="500",
            message="wisdom_capital has no price_api_key or price_api_secret in its settings, so the market data session cannot be established.",
        )

    response = self.post(
        url=MARKET_DATA_SESSION_URL,
        headers={'Content-Type': 'application/json'},
        json={
            "appKey": application_key,
            "secretKey": application_secret,
            "source": "WEBAPI",
        },
    )
    result = (response or {}).get("data") or {}
    if not result.get("token"):
        raise WisdomCapitalAPIException(
            code="500",
            message=f"Wisdom Capital's market data login did not return a token: {str(response)[:200]}",
        )

    self._logger.info("Logged in to Wisdom Capital's market data API.")
    self._store_login({
        "market_data_access_token": result["token"],
        "market_data_user_id": result.get("userID"),
        "market_data_last_login": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    })
    return {
        "access_token": result["token"],
        "user_id": result.get("userID"),
    }

_store_login

_store_login(fields)

Merges freshly minted session fields into the shared login, MongoDB first and Redis second.

The two tokens are minted by different paths and at different times, so a login is merged into whatever is stored rather than replacing it. Without that, establishing one session would erase the other's token for every process reading the same document.

Parameters:

Name Type Description Default
fields dict

The fields to set on the last_login document.

required

Returns:

Type Description
dict

The stored login after the merge, also left in self._last_login.

Source code in stock_brokers/api/wisdom_capital.py
def _store_login(self, fields):
    """Merges freshly minted session fields into the shared login, MongoDB first and Redis second.

    The two tokens are minted by different paths and at different times, so a login is merged into whatever is stored rather than replacing it. Without that, establishing one session would erase the other's token for every process reading the same document.

    Args:
        fields (dict): The fields to set on the `last_login` document.

    Returns:
        dict: The stored login after the merge, also left in `self._last_login`.
    """
    login = dict(self._current_login() or {})
    login["broker_name"] = "wisdom_capital"
    login.update(fields)

    self._mongo_db["last_login"].replace_one({"broker_name": "wisdom_capital"}, login, upsert=True)
    self._cache.hset("last_login", "wisdom_capital", json_lib.dumps(login))
    self._last_login = login
    return login

_request

_request(method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False)

Private generic REST API request to Broker API server. This is specialized into GET, POST, PUT, PATCH and DELETE by public methods.

  • method: HTTP method to use for the request.
  • `url
  • params: Dictionary of parameters to be sent as part of the request.
  • data: Dictionary, bytes, or file-like object to send in the body of the request.
  • headers: Dictionary of HTTP headers to be sent with the request.
  • cookies: Dictionary of cookies to be sent with the request.
  • files: Dictionary of files to be sent with the request.
  • auth: Authentication tuple or object to be sent with the request.
  • timeout: Timeout value for the request.
  • allow_redirects: Boolean value indicating whether redirects should be allowed.
  • proxies: Dictionary of proxy settings for the request.
  • hooks: Dictionary of hooks to be called during the request lifecycle.
  • stream: Boolean value indicating whether response content should be streamed.
  • verify: Boolean value indicating whether SSL certificate verification should be performed.
  • cert: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
  • json: JSON data to send in the body of the request.
  • verbose: If set to True, the request and response are logged.
Source code in stock_brokers/api/wisdom_capital.py
def _request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False):
    """
    Private generic REST API request to Broker API server.
    This is specialized into GET, POST, PUT, PATCH and DELETE by public methods.

    - `method`: HTTP method to use for the request.
    - `url
    - `params`: Dictionary of parameters to be sent as part of the request.
    - `data`: Dictionary, bytes, or file-like object to send in the body of the request.
    - `headers`: Dictionary of HTTP headers to be sent with the request.
    - `cookies`: Dictionary of cookies to be sent with the request.
    - `files`: Dictionary of files to be sent with the request.
    - `auth`: Authentication tuple or object to be sent with the request.
    - `timeout`: Timeout value for the request.
    - `allow_redirects`: Boolean value indicating whether redirects should be allowed.
    - `proxies`: Dictionary of proxy settings for the request.
    - `hooks`: Dictionary of hooks to be called during the request lifecycle.
    - `stream`: Boolean value indicating whether response content should be streamed.
    - `verify`: Boolean value indicating whether SSL certificate verification should be performed.
    - `cert`: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
    - `json`: JSON data to send in the body of the request.
    - `verbose`: If set to True, the request and response are logged.
    """
    self._current_login()
    if headers is None:
        headers = {
            'Content-Type': 'application/json',
            'authorization': self._last_login['access_token']
        }        

    if verify is None:
        verify = self._VERIFY_SSL

    if verbose == True:
        rest_api_message = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f')} - requests.request(method='{method}', url='{url}', params={params}, data={data}, headers={headers}, cookies={cookies}, files={files}, auth={auth}, timeout={timeout}, allow_redirects={allow_redirects}, proxies={proxies}, hooks={hooks}, stream={stream}, verify={verify}, cert={cert}, json={json})"
        self._cache.rpush('broker_api_calls', rest_api_message)
        self._logger.info(rest_api_message)        

    response = requests.request(method=method, url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json)

    if response.status_code < 300:
        content = {
            "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
            "status": "success",
            "code": response.status_code
        }

        if "json" in response.headers["Content-Type"]:
            if "result" in response.json():
                content["data"] = response.json()["result"]
            else:
                content["data"] = response.json()
        elif "text" in response.headers["Content-Type"]:
            try:
                json_content = json_lib.loads(response.content.decode("utf-8").strip())
                if "result" in json_content:
                    content["data"] = json_content["result"]
                else:
                    content["data"] = json_content
            except json_lib.JSONDecodeError:
                content["data"] = response.content.decode("utf-8").strip()
        else:
            content["data"] = response.content.decode("utf-8").strip()
        return content
    else:
        if "json" in response.headers["Content-Type"]:
            json_content = response.json()
            if "code" in json_content and "description" in json_content:
                raise WisdomCapitalAPIException(code=json_content["code"], message=json_content["description"])
            elif "errorType" in json_content and "errorMessage" in json_content:
                raise WisdomCapitalAPIException(code=json_content["errorType"], message=json_content["errorMessage"])
            else:
                raise WisdomCapitalAPIException(code=response.status_code, message=json_content)
        elif "text" in response.headers["Content-Type"]:
            try:
                json_content = json_lib.loads(response.content.decode("utf-8").strip())
                if "code" in json_content and "description" in json_content:
                    raise WisdomCapitalAPIException(code=json_content["code"], message=json_content["description"])
                elif "errorType" in json_content and "errorMessage" in json_content:
                    raise WisdomCapitalAPIException(code=json_content["errorType"], message=json_content["errorMessage"])
                else:
                    raise WisdomCapitalAPIException(code=response.status_code, message=json_content)
            except json_lib.JSONDecodeError:
                raise WisdomCapitalAPIException(code=response.status_code, message=response.content.decode("utf-8").strip())
        else:
            raise WisdomCapitalAPIException(code=response.status_code, message=response.content.decode("utf-8").strip())

_current_login

_current_login()

The login in force right now, read on every request rather than once at construction.

Any process that logs a broker in writes the new login to MongoDB and then to the Redis last_login hash. Reading that hash here, on every request, means a token another process has just obtained is used by this object's very next request, rather than a token loaded at construction being sent long after it has been replaced.

Redis is the one place a token is read from, and a login the only thing that writes it. Constructors do not copy MongoDB into it, because that would race a login: a process that read MongoDB just before another logged in, and wrote Redis just after, would put the old token back for everyone.

The newer of this object's login and Redis's is used, by their last_login time. The shared login normally wins. This object's own wins only when it is genuinely newer - a login this object has just made, or one whose write to Redis failed while its write to MongoDB, which this object was built from, succeeded. When either time cannot be read, Redis is trusted.

When Redis holds nothing for the broker, the login is read from MongoDB and written into Redis only if that field is still empty, so it can fill the cache but never overwrite a login that lands in between. When neither store can be read, the object keeps what it has.

Returns:

Type Description
dict | None

The last_login document now in force, also left in self._last_login.

Source code in stock_brokers/api/base.py
def _current_login(self):
    """
    The login in force right now, read on every request rather than once at construction.

    Any process that logs a broker in writes the new login to MongoDB and then to the Redis
    `last_login` hash. Reading that hash here, on every request, means a token another process
    has just obtained is used by this object's very next request, rather than a token loaded at
    construction being sent long after it has been replaced.

    Redis is the one place a token is read from, and a login the only thing that writes it.
    Constructors do not copy MongoDB into it, because that would race a login: a process that
    read MongoDB just before another logged in, and wrote Redis just after, would put the old
    token back for everyone.

    The newer of this object's login and Redis's is used, by their `last_login` time. The
    shared login normally wins. This object's own wins only when it is genuinely newer - a
    login this object has just made, or one whose write to Redis failed while its write to
    MongoDB, which this object was built from, succeeded. When either time cannot be read,
    Redis is trusted.

    When Redis holds nothing for the broker, the login is read from MongoDB and written into
    Redis only if that field is still empty, so it can fill the cache but never overwrite a
    login that lands in between. When neither store can be read, the object keeps what it has.

    Returns:
        dict | None: The `last_login` document now in force, also left in `self._last_login`.
    """
    try:
        raw = self._cache.hget('last_login', self._broker_name)
        shared = json.loads(raw) if raw else None
    except Exception:
        shared = None

    if not shared:
        try:
            shared = self._mongo_db['last_login'].find_one({'broker_name': self._broker_name}, {'_id': 0})
        except Exception:
            shared = None
        if shared:
            try:
                self._cache.hsetnx('last_login', self._broker_name, json.dumps(shared))
            except Exception:
                pass

    if shared:
        own_moment = self._login_moment(self._last_login)
        shared_moment = self._login_moment(shared)
        if own_moment is None or shared_moment is None or shared_moment >= own_moment:
            self._last_login = shared
    return self._last_login

_login_moment staticmethod

_login_moment(document)

When a login document was made, or None when that cannot be read.

Brokers stamp last_login with local time, some with microseconds and some without.

Parameters:

Name Type Description Default
document dict | None

A last_login document.

required

Returns:

Type Description
datetime | None

The moment of the login.

Source code in stock_brokers/api/base.py
@staticmethod
def _login_moment(document):
    """
    When a login document was made, or None when that cannot be read.

    Brokers stamp `last_login` with local time, some with microseconds and some without.

    Args:
        document (dict | None): A `last_login` document.

    Returns:
        datetime | None: The moment of the login.
    """
    stamp = (document or {}).get('last_login')
    for form in ('%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S'):
        try:
            return datetime.strptime(str(stamp), form)
        except (TypeError, ValueError):
            continue
    return None

get

get(url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False)

HTTP GET API call to Broker's API server. This queries the Broker's API server for some information. Most responses are JSON encoded. Only a few are CSV encoded.

  • url: URL of the API endpoint.
  • params: Dictionary of parameters to be sent as part of the request.
  • data: Dictionary, bytes, or file-like object to send in the body of the request.
  • headers: Dictionary of HTTP headers to be sent with the request.
  • cookies: Dictionary of cookies to be sent with the request.
  • files: Dictionary of files to be sent with the request.
  • auth: Authentication tuple or object to be sent with the request.
  • timeout: Timeout value for the request.
  • allow_redirects: Boolean value indicating whether redirects should be allowed.
  • proxies: Dictionary of proxy settings for the request.
  • hooks: Dictionary of hooks to be called during the request lifecycle.
  • stream: Boolean value indicating whether response content should be streamed.
  • verify: Boolean value indicating whether SSL certificate verification should be performed.
  • cert: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
  • json: JSON data to send in the body of the request.
  • verbose: If set to True, the request and response are logged.

Example usage

From example_1_writing_a_broker_class.py
fields = self.hashes.get(name, {})
From example_1_writing_a_broker_class.py
return fields.get(key)
From example_1_writing_a_broker_class.py
self.show('get', api.get(url=url))
Source code in stock_brokers/api/base.py
def get(self, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False):
    """
    HTTP GET  API call to Broker's API server.
    This queries the Broker's API server for some information. Most responses are JSON encoded. Only a few are CSV encoded.

    - `url`: URL of the API endpoint.
    - `params`: Dictionary of parameters to be sent as part of the request.
    - `data`: Dictionary, bytes, or file-like object to send in the body of the request.
    - `headers`: Dictionary of HTTP headers to be sent with the request.
    - `cookies`: Dictionary of cookies to be sent with the request.
    - `files`: Dictionary of files to be sent with the request.
    - `auth`: Authentication tuple or object to be sent with the request.
    - `timeout`: Timeout value for the request.
    - `allow_redirects`: Boolean value indicating whether redirects should be allowed.
    - `proxies`: Dictionary of proxy settings for the request.
    - `hooks`: Dictionary of hooks to be called during the request lifecycle.
    - `stream`: Boolean value indicating whether response content should be streamed.
    - `verify`: Boolean value indicating whether SSL certificate verification should be performed.
    - `cert`: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
    - `json`: JSON data to send in the body of the request.
    - `verbose`: If set to True, the request and response are logged.
    """
    data = self._request(method="GET", url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json)
    return data

post

post(url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False)

HTTP POST API call to Broker's API server. This carries out some action using Broker's API server such as placing an order. Responses are JSON encoded.

  • url: URL of the API endpoint.
  • params: Dictionary of parameters to be sent as part of the request.
  • data: Dictionary, bytes, or file-like object to send in the body of the request.
  • headers: Dictionary of HTTP headers to be sent with the request.
  • cookies: Dictionary of cookies to be sent with the request.
  • files: Dictionary of files to be sent with the request.
  • auth: Authentication tuple or object to be sent with the request.
  • timeout: Timeout value for the request.
  • allow_redirects: Boolean value indicating whether redirects should be allowed.
  • proxies: Dictionary of proxy settings for the request.
  • hooks: Dictionary of hooks to be called during the request lifecycle.
  • stream: Boolean value indicating whether response content should be streamed.
  • verify: Boolean value indicating whether SSL certificate verification should be performed.
  • cert: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
  • json: JSON data to send in the body of the request.
  • verbose: If set to True, the request and response are logged.

Example usage

From example_1_writing_a_broker_class.py
self.show('post', api.post(url=url, json=new_watchlist))
Source code in stock_brokers/api/base.py
def post(self, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False):
    """
    HTTP POST  API call to Broker's API server.
    This carries out some action using Broker's API server such as placing an order. Responses are JSON encoded.

    - `url`: URL of the API endpoint.
    - `params`: Dictionary of parameters to be sent as part of the request.
    - `data`: Dictionary, bytes, or file-like object to send in the body of the request.
    - `headers`: Dictionary of HTTP headers to be sent with the request.
    - `cookies`: Dictionary of cookies to be sent with the request.
    - `files`: Dictionary of files to be sent with the request.
    - `auth`: Authentication tuple or object to be sent with the request.
    - `timeout`: Timeout value for the request.
    - `allow_redirects`: Boolean value indicating whether redirects should be allowed.
    - `proxies`: Dictionary of proxy settings for the request.
    - `hooks`: Dictionary of hooks to be called during the request lifecycle.
    - `stream`: Boolean value indicating whether response content should be streamed.
    - `verify`: Boolean value indicating whether SSL certificate verification should be performed.
    - `cert`: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
    - `json`: JSON data to send in the body of the request.
    - `verbose`: If set to True, the request and response are logged.
    """
    data = self._request(method="POST", url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json)
    return data

put

put(url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False)

HTTP PUT API call to Broker's API server. This modifies something using Broker's API server such as modifying an order. Responses are JSON encoded.

  • url: URL of the API endpoint.
  • params: Dictionary of parameters to be sent as part of the request.
  • data: Dictionary, bytes, or file-like object to send in the body of the request.
  • headers: Dictionary of HTTP headers to be sent with the request.
  • cookies: Dictionary of cookies to be sent with the request.
  • files: Dictionary of files to be sent with the request.
  • auth: Authentication tuple or object to be sent with the request.
  • timeout: Timeout value for the request.
  • allow_redirects: Boolean value indicating whether redirects should be allowed.
  • proxies: Dictionary of proxy settings for the request.
  • hooks: Dictionary of hooks to be called during the request lifecycle.
  • stream: Boolean value indicating whether response content should be streamed.
  • verify: Boolean value indicating whether SSL certificate verification should be performed.
  • cert: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
  • json: JSON data to send in the body of the request.
  • verbose: If set to True, the request and response are logged.

Example usage

From example_1_writing_a_broker_class.py
self.show('put', api.put(url=url, json=replacement))
Source code in stock_brokers/api/base.py
def put(self, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False):
    """
    HTTP PUT  API call to Broker's API server.
    This modifies something using Broker's API server such as modifying an order. Responses are JSON encoded.

    - `url`: URL of the API endpoint.
    - `params`: Dictionary of parameters to be sent as part of the request.
    - `data`: Dictionary, bytes, or file-like object to send in the body of the request.
    - `headers`: Dictionary of HTTP headers to be sent with the request.
    - `cookies`: Dictionary of cookies to be sent with the request.
    - `files`: Dictionary of files to be sent with the request.
    - `auth`: Authentication tuple or object to be sent with the request.
    - `timeout`: Timeout value for the request.
    - `allow_redirects`: Boolean value indicating whether redirects should be allowed.
    - `proxies`: Dictionary of proxy settings for the request.
    - `hooks`: Dictionary of hooks to be called during the request lifecycle.
    - `stream`: Boolean value indicating whether response content should be streamed.
    - `verify`: Boolean value indicating whether SSL certificate verification should be performed.
    - `cert`: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
    - `json`: JSON data to send in the body of the request.
    - `verbose`: If set to True, the request and response are logged.
    """
    data = self._request(method="PUT", url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json)
    return data

patch

patch(url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False)

HTTP PATCH API call to Broker's API server. This updates something using Broker's API server such as modifying an order. Responses are JSON encoded.

  • url: URL of the API endpoint.
  • params: Dictionary of parameters to be sent as part of the request.
  • data: Dictionary, bytes, or file-like object to send in the body of the request.
  • headers: Dictionary of HTTP headers to be sent with the request.
  • cookies: Dictionary of cookies to be sent with the request.
  • files: Dictionary of files to be sent with the request.
  • auth: Authentication tuple or object to be sent with the request.
  • timeout: Timeout value for the request.
  • allow_redirects: Boolean value indicating whether redirects should be allowed.
  • proxies: Dictionary of proxy settings for the request.
  • hooks: Dictionary of hooks to be called during the request lifecycle.
  • stream: Boolean value indicating whether response content should be streamed.
  • verify: Boolean value indicating whether SSL certificate verification should be performed.
  • cert: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
  • json: JSON data to send in the body of the request.
  • verbose: If set to True, the request and response are logged.

Example usage

From example_1_writing_a_broker_class.py
self.show('patch', api.patch(url=url, json=rename))
Source code in stock_brokers/api/base.py
def patch(self, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False):
    """
    HTTP PATCH  API call to Broker's API server.
    This updates something using Broker's API server such as modifying an order. Responses are JSON encoded.

    - `url`: URL of the API endpoint.
    - `params`: Dictionary of parameters to be sent as part of the request.
    - `data`: Dictionary, bytes, or file-like object to send in the body of the request.
    - `headers`: Dictionary of HTTP headers to be sent with the request.
    - `cookies`: Dictionary of cookies to be sent with the request.
    - `files`: Dictionary of files to be sent with the request.
    - `auth`: Authentication tuple or object to be sent with the request.
    - `timeout`: Timeout value for the request.
    - `allow_redirects`: Boolean value indicating whether redirects should be allowed.
    - `proxies`: Dictionary of proxy settings for the request.
    - `hooks`: Dictionary of hooks to be called during the request lifecycle.
    - `stream`: Boolean value indicating whether response content should be streamed.
    - `verify`: Boolean value indicating whether SSL certificate verification should be performed.
    - `cert`: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
    - `json`: JSON data to send in the body of the request.
    - `verbose`: If set to True, the request and response are logged.
    """
    data = self._request(method="PATCH", url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json)
    return data

delete

delete(url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False)

HTTP DELETE API call to Broker's API server. This deletes something using Broker's API server such as cancelling an order or invalidating an access token. Responses are JSON encoded.

  • url: URL of the API endpoint.
  • params: Dictionary of parameters to be sent as part of the request.
  • data: Dictionary, bytes, or file-like object to send in the body of the request.
  • headers: Dictionary of HTTP headers to be sent with the request.
  • cookies: Dictionary of cookies to be sent with the request.
  • files: Dictionary of files to be sent with the request.
  • auth: Authentication tuple or object to be sent with the request.
  • timeout: Timeout value for the request.
  • allow_redirects: Boolean value indicating whether redirects should be allowed.
  • proxies: Dictionary of proxy settings for the request.
  • hooks: Dictionary of hooks to be called during the request lifecycle.
  • stream: Boolean value indicating whether response content should be streamed.
  • verify: Boolean value indicating whether SSL certificate verification should be performed.
  • cert: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
  • json: JSON data to send in the body of the request.
  • verbose: If set to True, the request and response are logged.

Example usage

From example_1_writing_a_broker_class.py
self.show('delete', api.delete(url=url))
Source code in stock_brokers/api/base.py
def delete(self, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=None, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, verbose=False):
    """
    HTTP DELETE  API call to Broker's API server.
    This deletes something using Broker's API server such as cancelling an order or invalidating an access token. Responses are JSON encoded.

    - `url`: URL of the API endpoint.
    - `params`: Dictionary of parameters to be sent as part of the request.
    - `data`: Dictionary, bytes, or file-like object to send in the body of the request.
    - `headers`: Dictionary of HTTP headers to be sent with the request.
    - `cookies`: Dictionary of cookies to be sent with the request.
    - `files`: Dictionary of files to be sent with the request.
    - `auth`: Authentication tuple or object to be sent with the request.
    - `timeout`: Timeout value for the request.
    - `allow_redirects`: Boolean value indicating whether redirects should be allowed.
    - `proxies`: Dictionary of proxy settings for the request.
    - `hooks`: Dictionary of hooks to be called during the request lifecycle.
    - `stream`: Boolean value indicating whether response content should be streamed.
    - `verify`: Boolean value indicating whether SSL certificate verification should be performed.
    - `cert`: Certificate file or tuple containing certificate and key files for client-side SSL certificate verification.
    - `json`: JSON data to send in the body of the request.
    - `verbose`: If set to True, the request and response are logged.
    """
    data = self._request(method="DELETE", url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json)
    return data