Skip to content

groww

Groww's two websockets, both NATS over a websocket at wss://socket-api.groww.in: the market feed and the account's order and position updates.

The login and the socket token. GrowwSession holds the GrowwAPI both sockets read the REST access token from, afresh on every connect, so a login made by any process is picked up. Before connecting, a socket generates a fresh ed25519 key pair and exchanges its public key, in NATS NKEY form, for a short-lived socket JWT and a subscription id at POST https://api.groww.in/v1/api/apex/v1/socket/token/create/, authorized with the access token. HTTP 401 or 403 there means the access token is dead, which the socket treats as a refused login. The quote sockets log in again one at a time, and only when no other socket or process has already replaced the token that failed; the order socket logs in again whenever it is refused, without that check, as it always has.

NATS. The server opens with an INFO frame carrying a nonce; the socket answers CONNECT with the JWT and the nonce signed by its private key, then PING, then one SUB per subject. NATS is line based: control lines end with CRLF, and a MSG line is followed by a payload of the declared length, and a websocket frame may split or join those, so GrowwNatsBuffer buffers the stream and hands back only whole messages. A server PING is answered with PONG, and an -ERR naming an authorization failure is a refused login.

Quotes. GrowwQuotesSocket subscribes a price subject and a depth subject per instrument, such as /ld/eq/nse/price_detailed.2885 and /ld/eq/nse/book.2885, with /ld/fo/... for derivatives. Payloads are protobuf StocksSocketResponseProtoDto messages, whose classes are built at runtime from Groww's file descriptor, carried here serialized, in a descriptor pool of their own. Price and depth arrive separately and are merged per instrument. Prices are in paise and are divided into rupees, and quantities arrive as doubles and are rounded to whole numbers. An instrument without a name takes the symbol Groww puts in the payload. The quote socket gives up only after six more failed connects, not on the first refusal after logging in again.

Order updates. GrowwOrderUpdatesSocket subscribes the account's subjects, keyed by the subscription id: equity and derivatives orders (OrderDetailsBroadCastDto) and derivatives positions (PositionDetailProto). Each message is decoded to a plain dictionary with Groww's own field names, enum fields by name, and every field present even when zero.

Neither socket writes Redis. The quotes socket hands each frame's ticks to on_ticks, and the order socket hands each frame's decoded orders and positions to on_updates; the scripts in bin/groww/ do the writing.

Classes:

Name Description
GrowwSocketRefused

Groww refused the socket token request, which means the access token is dead.

GrowwMessageClasses

The protobuf message classes Groww's payloads decode into, built from the embedded file descriptors.

GrowwNkeyPair

An ed25519 key pair in NATS NKEY form: the public key as a user NKEY, and nonce signing.

GrowwNatsBuffer

The NATS byte stream of one connection, buffered so that only whole lines and messages come out.

GrowwSession

The Groww login every socket authenticates with, logged in again by one socket at a time.

GrowwQuotesSocket

One Groww NATS websocket carrying the price and depth of a batch of instruments.

GrowwOrderUpdatesSocket

Groww's order and position update stream for the account, over NATS.

GrowwSocketRefused

Bases: Exception

Groww refused the socket token request, which means the access token is dead.

Example programs

examples/stock_brokers/websockets/groww/GrowwSocketRefused/example_1_raising_and_catching.py
"""Raises and catches `GrowwSocketRefused`, the error that marks a dead Groww access token.

`GrowwSocketRefused` is raised when Groww refuses a socket token request with HTTP 401 or 403, which means the access token is dead. Both Groww sockets catch it in their connect step and turn it into a refused login, so their reconnect loops log in again. It is a plain `Exception` subclass whose message says the HTTP status and the start of Groww's answer.

This program raises it with the message shape the real code uses and catches it, both as itself and as a plain `Exception`. It needs no stand-ins.

Notice that the class name is what tells a refused login apart from any other failure.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwSocketRefused/example_1_raising_and_catching.py
"""


from stock_brokers.websockets.groww import (
    GrowwSocketRefused,
)


class RaisingAndCatchingExample:
    """Raises and catches a Groww socket refusal.

    Attributes:
        message (str): The message the refusal carries.
    """

    def __init__(self):
        """Builds the message the way the session does.

        Returns:
            None: This method returns nothing.
        """
        self.message = 'HTTP 403: {"message": "Forbidden"}'

    def refuse(self):
        """Raises the refusal.

        Returns:
            None: This method returns nothing.

        Raises:
            GrowwSocketRefused: Always.
        """
        raise GrowwSocketRefused(self.message)

    def run(self):
        """Catches the refusal twice, once by its own class and once as any exception.

        Returns:
            None: This method returns nothing.
        """
        try:
            self.refuse()
        except GrowwSocketRefused as refusal:
            print(f'Caught as a refused login: {refusal}')
        try:
            self.refuse()
        except Exception as error:
            print(f'Caught as {type(error).__name__}, a subclass of Exception: {isinstance(error, Exception)}')


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

It prints:

Caught as a refused login: HTTP 403: {"message": "Forbidden"}
Caught as GrowwSocketRefused, a subclass of Exception: True
examples/stock_brokers/websockets/groww/GrowwSocketRefused/example_2_from_the_token_endpoint.py
"""Catches the `GrowwSocketRefused` a Groww session raises when the socket token endpoint answers HTTP 401.

In practice `GrowwSocketRefused` comes from `GrowwSession.socket_token_payload`, which raises it for HTTP 401 or 403 and lets any other error status raise as `requests` does. A socket catches it and logs in again instead of counting an ordinary connection failure.

Constructing the real `GrowwAPI` logs in to Groww, so the program registers a stand-in module under `stock_brokers.api.groww`, and a stand-in `requests` package answers the socket token request with HTTP 401.

Notice that the message carries the status and Groww's answer.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwSocketRefused/example_2_from_the_token_endpoint.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwNkeyPair,
    GrowwSession,
    GrowwSocketRefused,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class FromTheTokenEndpointExample:
    """Requests a socket token with a dead access token.

    Attributes:
        session (GrowwSession): The session that makes the request.
    """

    def __init__(self):
        """Registers the stand-ins and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            401,
        ])
        self.session = GrowwSession(logging.getLogger('groww.session'))

    def run(self):
        """Requests a socket token and prints the refusal.

        Returns:
            None: This method returns nothing.
        """
        try:
            self.session.socket_token_payload(GrowwNkeyPair())
        except GrowwSocketRefused as refusal:
            print(f'GrowwSocketRefused: {refusal}')


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

It prints:

Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 401
GrowwSocketRefused: HTTP 401: {"message": "Unauthorized"}

GrowwMessageClasses

The protobuf message classes Groww's payloads decode into, built from the embedded file descriptors.

Example programs

examples/stock_brokers/websockets/groww/GrowwMessageClasses/example_1_market_feed_message.py
"""Builds Groww's market feed protobuf class, round-trips a price message through bytes, and turns it into a dictionary.

Groww's market feed payloads are protobuf `StocksSocketResponseProtoDto` messages. `GrowwMessageClasses.stocks_response_class` builds that class at runtime from Groww's file descriptor, which the module carries serialized, in a descriptor pool of its own so it cannot clash with any other protobuf classes. `as_dict` turns any decoded message into a plain dictionary with Groww's own field names, enums by name and every field present.

This program fills in a price message for RELIANCE, serializes it as the server would, decodes the bytes with the class, and prints the dictionary. It needs no stand-ins.

Notice that fields the program never set, such as `openInterest`, still appear with zero, and that `segment` and `exchange` come out as the names `CASH` and `NSE`.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwMessageClasses/example_1_market_feed_message.py
"""


from stock_brokers.websockets.groww import (
    GrowwMessageClasses,
)


class MarketFeedMessageExample:
    """Round-trips one Groww market feed message.

    Attributes:
        message_classes (GrowwMessageClasses): The class builder being shown.
    """

    def __init__(self):
        """Builds the class builder.

        Returns:
            None: This method returns nothing.
        """
        self.message_classes = GrowwMessageClasses()

    def run(self):
        """Builds, serializes, decodes and prints a price message.

        Returns:
            None: This method returns nothing.
        """
        response_class = self.message_classes.stocks_response_class()
        print(f'Class: {response_class.DESCRIPTOR.full_name}')
        message = response_class()
        message.symbol = 'RELIANCE'
        message.segment = 0
        message.exchange = 1
        message.stockLivePrice.ltp = 295050
        message.stockLivePrice.close = 291000
        message.stockLivePrice.volume = 1204500
        payload = message.SerializeToString()
        print(f'Serialized to {len(payload)} bytes')
        decoded = response_class.FromString(payload)
        dictionary = self.message_classes.as_dict(decoded)
        print(f"symbol={dictionary['symbol']} segment={dictionary['segment']} exchange={dictionary['exchange']}")
        for field, value in sorted(dictionary['stockLivePrice'].items()):
            print(f'  {field}: {value}')


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

It prints:

Class: stocksData.response.StocksSocketResponseProtoDto
Serialized to 41 bytes
symbol=RELIANCE segment=CASH exchange=NSE
  avgPrice: 0.0
  bidQty: 0.0
  close: 291000.0
  high: 0.0
  highPriceRange: 0.0
  highTradeRange: 0.0
  low: 0.0
  lowPriceRange: 0.0
  lowTradeRange: 0.0
  ltp: 295050.0
  offerQty: 0.0
  open: 0.0
  openInterest: 0.0
  tsInMillis: 0.0
  value: 0.0
  volume: 1204500.0
examples/stock_brokers/websockets/groww/GrowwMessageClasses/example_2_order_and_position_messages.py
"""Builds Groww's order and position protobuf classes and decodes one of each into a dictionary.

The order update stream carries two kinds of protobuf payload: `OrderDetailsBroadCastDto` for equity and derivatives orders and `PositionDetailProto` for derivatives positions. `GrowwMessageClasses.order_and_position_classes` builds both from their file descriptors in one pool and returns them as a pair, and `as_dict` turns a decoded message into the dictionary the order socket hands on.

This program builds an executed INFY buy and a NIFTY call position, serializes and decodes each, and prints selected fields of the dictionaries. It needs no stand-ins.

Notice that the order status and side come out by name (`EXECUTED`, `B`), that the 64 bit price comes out as text, as protobuf JSON always writes 64 bit integers, and that the position keeps its nested `symbolData` and `positionInfo`.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwMessageClasses/example_2_order_and_position_messages.py
"""


from stock_brokers.websockets.groww import (
    GrowwMessageClasses,
)


class OrderAndPositionMessagesExample:
    """Round-trips one Groww order message and one position message.

    Attributes:
        message_classes (GrowwMessageClasses): The class builder being shown.
    """

    def __init__(self):
        """Builds the class builder.

        Returns:
            None: This method returns nothing.
        """
        self.message_classes = GrowwMessageClasses()

    def run(self):
        """Builds, serializes, decodes and prints an order and a position.

        Returns:
            None: This method returns nothing.
        """
        order_class, position_class = self.message_classes.order_and_position_classes()
        print(f'Classes: {order_class.DESCRIPTOR.full_name} and {position_class.DESCRIPTOR.full_name}')
        broadcast = order_class()
        broadcast.orderDetailUpdateDto.growwOrderId = 'GMK2609250001'
        broadcast.orderDetailUpdateDto.qty = 10
        broadcast.orderDetailUpdateDto.filledQty = 10
        broadcast.orderDetailUpdateDto.price = 150000
        broadcast.orderDetailUpdateDto.orderStatus = 6
        order = self.message_classes.as_dict(order_class.FromString(broadcast.SerializeToString()))
        detail = order['orderDetailUpdateDto']
        print(f"Order {detail['growwOrderId']}: status={detail['orderStatus']} side={detail['buySell']} filled={detail['filledQty']} price={detail['price']!r}")
        position = position_class()
        position.symbolData.contractId = 'NIFTY26SEP25000CE'
        position.symbolData.equityType = 2
        position.positionInfo.NSE.creditQty = 75
        decoded = self.message_classes.as_dict(position_class.FromString(position.SerializeToString()))
        print(f"Position {decoded['symbolData']['contractId']}: type={decoded['symbolData']['equityType']} NSE={decoded['positionInfo']['NSE']}")


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

It prints:

Classes: OrderDetailsBroadCastDto and groww.protobuf.fno.dto.fnoData.PositionDetailProto
Order GMK2609250001: status=EXECUTED side=B filled=10 price='150000'
Position NIFTY26SEP25000CE: type=OPTION NSE={'creditQty': 75.0, 'creditPrice': 0.0, 'debitQty': 0.0, 'debitPrice': 0.0}

Methods:

Name Description
stocks_response_class

The stocksData.response.StocksSocketResponseProtoDto class the market feed's payloads decode into.

order_and_position_classes

The OrderDetailsBroadCastDto and PositionDetailProto classes the order stream's payloads decode into.

as_dict

A protobuf message as a JSON-ready dictionary: Groww's field names, enums by name, every field present.

stocks_response_class

stocks_response_class()

The stocksData.response.StocksSocketResponseProtoDto class the market feed's payloads decode into.

Returns:

Type Description
type

The message class.

Example usage

From example_1_market_feed_message.py
response_class = self.message_classes.stocks_response_class()
Source code in stock_brokers/websockets/groww.py
def stocks_response_class(self):
    """
    The `stocksData.response.StocksSocketResponseProtoDto` class the market feed's payloads decode into.

    Returns:
        type: The message class.
    """
    from google.protobuf import descriptor_pb2, descriptor_pool, message_factory

    pool = descriptor_pool.DescriptorPool()
    pool.Add(descriptor_pb2.FileDescriptorProto.FromString(base64.b64decode("".join(STOCKS_DESCRIPTOR))))
    return message_factory.GetMessageClass(pool.FindMessageTypeByName("stocksData.response.StocksSocketResponseProtoDto"))

order_and_position_classes

order_and_position_classes()

The OrderDetailsBroadCastDto and PositionDetailProto classes the order stream's payloads decode into.

Returns:

Type Description
tuple

The order message class and the position message class.

Example usage

From example_2_order_and_position_messages.py
order_class, position_class = self.message_classes.order_and_position_classes()
Source code in stock_brokers/websockets/groww.py
def order_and_position_classes(self):
    """
    The `OrderDetailsBroadCastDto` and `PositionDetailProto` classes the order stream's payloads decode into.

    Returns:
        tuple: The order message class and the position message class.
    """
    from google.protobuf import descriptor_pb2, descriptor_pool, message_factory

    pool = descriptor_pool.DescriptorPool()
    for serialized in (ORDERS_DESCRIPTOR, POSITIONS_DESCRIPTOR):
        pool.Add(descriptor_pb2.FileDescriptorProto.FromString(base64.b64decode("".join(serialized))))
    order_class = message_factory.GetMessageClass(pool.FindMessageTypeByName("OrderDetailsBroadCastDto"))
    position_class = message_factory.GetMessageClass(pool.FindMessageTypeByName("groww.protobuf.fno.dto.fnoData.PositionDetailProto"))
    return order_class, position_class

as_dict

as_dict(message)

A protobuf message as a JSON-ready dictionary: Groww's field names, enums by name, every field present.

Parameters:

Name Type Description Default
message Message

The decoded message.

required

Returns:

Type Description
dict

The message.

Example usage

From example_1_market_feed_message.py
dictionary = self.message_classes.as_dict(decoded)
From example_2_order_and_position_messages.py
order = self.message_classes.as_dict(order_class.FromString(broadcast.SerializeToString()))
From example_2_order_and_position_messages.py
decoded = self.message_classes.as_dict(position_class.FromString(position.SerializeToString()))
Source code in stock_brokers/websockets/groww.py
def as_dict(self, message):
    """
    A protobuf message as a JSON-ready dictionary: Groww's field names, enums by name, every field present.

    Args:
        message (google.protobuf.message.Message): The decoded message.

    Returns:
        dict: The message.
    """
    from google.protobuf import json_format

    return json_format.MessageToDict(message, preserving_proto_field_name=True, always_print_fields_with_no_presence=True)

GrowwNkeyPair

GrowwNkeyPair()

An ed25519 key pair in NATS NKEY form: the public key as a user NKEY, and nonce signing.

Attributes:

Name Type Description
public_key str

The public key as a NATS user NKEY.

Generates a fresh key pair.

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/groww/GrowwNkeyPair/example_1_signing_a_nonce.py
"""Generates a Groww NATS key pair, signs a server nonce with it, and verifies the signature.

Before each Groww connection a socket generates a fresh ed25519 key pair, sends its public key as a NATS user NKEY to obtain a socket JWT, and later signs the nonce from the server's INFO line with the private key. `GrowwNkeyPair.public_key` is the NKEY: base32 of a user prefix byte, the raw public key and a CRC-16 checksum, so it always starts with `U` and has 56 characters. `signed_nonce` returns the signature base64url encoded without padding, as NATS expects.

The key is random on every run, so this program prints properties of the key and the signature rather than their values, and checks the signature by decoding the NKEY back to the raw public key and verifying with the `cryptography` package, the same library the class uses. It needs no stand-ins.

Notice that the signature verifies against the nonce it signed and not against a different one.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwNkeyPair/example_1_signing_a_nonce.py
"""

import base64

from cryptography.exceptions import (
    InvalidSignature,
)
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
    Ed25519PublicKey,
)
from stock_brokers.websockets.groww import (
    GrowwNkeyPair,
)


class SigningANonceExample:
    """Signs a nonce with a fresh key pair and verifies it.

    Attributes:
        key_pair (GrowwNkeyPair): The key pair being shown.
    """

    def __init__(self):
        """Generates the key pair.

        Returns:
            None: This method returns nothing.
        """
        self.key_pair = GrowwNkeyPair()

    def verifies(self, signature, nonce):
        """Says whether a signature verifies against a nonce with the key pair's public key.

        Args:
            signature (str): The base64url signature without padding.
            nonce (str): The nonce.

        Returns:
            bool: True when the signature is valid for the nonce.
        """
        public_key_bytes = base64.b32decode(self.key_pair.public_key)[1:33]
        public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
        raw_signature = base64.urlsafe_b64decode(signature + '==')
        try:
            public_key.verify(raw_signature, nonce.encode())
        except InvalidSignature:
            return False
        return True

    def run(self):
        """Prints the key's form, signs a nonce, and verifies the signature against two nonces.

        Returns:
            None: This method returns nothing.
        """
        public_key = self.key_pair.public_key
        print(f'Public key starts with {public_key[0]} and has {len(public_key)} characters')
        signature = self.key_pair.signed_nonce('example-nonce')
        print(f'Signature has {len(signature)} characters and no padding: {not signature.endswith("=")}')
        print(f"Verifies against the signed nonce: {self.verifies(signature, 'example-nonce')}")
        print(f"Verifies against another nonce: {self.verifies(signature, 'other-nonce')}")


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

It prints:

Public key starts with U and has 56 characters
Signature has 86 characters and no padding: True
Verifies against the signed nonce: True
Verifies against another nonce: False
examples/stock_brokers/websockets/groww/GrowwNkeyPair/example_2_fresh_keys_each_connection.py
"""Shows that every Groww key pair is fresh, and that a nonce signs the same whether given as text or bytes.

A Groww socket makes a new `GrowwNkeyPair` for every connection, so a socket JWT obtained for one connection cannot be reused with another key. `signed_nonce` accepts the nonce as text or bytes, encoding text first, and ed25519 signatures are deterministic, so the same key signs the same nonce identically either way.

The keys are random, so this program compares them rather than printing them. It needs no stand-ins.

Notice that the two public keys differ, and that the text and bytes nonces give the same signature.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwNkeyPair/example_2_fresh_keys_each_connection.py
"""


from stock_brokers.websockets.groww import (
    GrowwNkeyPair,
)


class FreshKeysEachConnectionExample:
    """Compares two key pairs and two ways of passing a nonce.

    Attributes:
        first_key_pair (GrowwNkeyPair): The key pair of one connection.
        second_key_pair (GrowwNkeyPair): The key pair of the next connection.
    """

    def __init__(self):
        """Generates two key pairs.

        Returns:
            None: This method returns nothing.
        """
        self.first_key_pair = GrowwNkeyPair()
        self.second_key_pair = GrowwNkeyPair()

    def run(self):
        """Prints whether the keys differ and whether the two nonce forms sign alike.

        Returns:
            None: This method returns nothing.
        """
        print(f'The two public keys differ: {self.first_key_pair.public_key != self.second_key_pair.public_key}')
        from_text = self.first_key_pair.signed_nonce('example-nonce')
        from_bytes = self.first_key_pair.signed_nonce(b'example-nonce')
        print(f'Text and bytes nonces sign alike: {from_text == from_bytes}')
        other_key = self.second_key_pair.signed_nonce('example-nonce')
        print(f'Another key signs differently: {from_text != other_key}')


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

It prints:

The two public keys differ: True
Text and bytes nonces sign alike: True
Another key signs differently: True

Methods:

Name Description
signed_nonce

A server nonce signed with the private key, base64url encoded without padding, as NATS expects.

Source code in stock_brokers/websockets/groww.py
def __init__(self):
    """
    Generates a fresh key pair.

    Returns:
        None: This method returns nothing.
    """
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
    from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

    self._private_key = Ed25519PrivateKey.generate()
    raw_public = self._private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
    body = bytes([NKEY_PREFIX_USER]) + raw_public
    self.public_key = base64.b32encode(body + self._crc16(body).to_bytes(2, "little")).rstrip(b"=").decode()

signed_nonce

signed_nonce(nonce)

A server nonce signed with the private key, base64url encoded without padding, as NATS expects.

Parameters:

Name Type Description Default
nonce str | bytes

The nonce from the INFO frame.

required

Returns:

Type Description
str

The signature.

Example usage

From example_1_signing_a_nonce.py
signature = self.key_pair.signed_nonce('example-nonce')
From example_2_fresh_keys_each_connection.py
from_text = self.first_key_pair.signed_nonce('example-nonce')
From example_2_fresh_keys_each_connection.py
from_bytes = self.first_key_pair.signed_nonce(b'example-nonce')
Source code in stock_brokers/websockets/groww.py
def signed_nonce(self, nonce):
    """
    A server nonce signed with the private key, base64url encoded without padding, as NATS expects.

    Args:
        nonce (str | bytes): The nonce from the INFO frame.

    Returns:
        str: The signature.
    """
    if isinstance(nonce, str):
        nonce = nonce.encode()
    signature = self._private_key.sign(nonce)
    return base64.urlsafe_b64encode(signature).rstrip(b"=").decode()

_crc16

_crc16(data)

CRC-16/XMODEM, as NATS computes it over NKEY material.

Parameters:

Name Type Description Default
data bytes

The bytes to checksum.

required

Returns:

Type Description
int

The checksum.

Source code in stock_brokers/websockets/groww.py
def _crc16(self, data):
    """
    CRC-16/XMODEM, as NATS computes it over NKEY material.

    Args:
        data (bytes): The bytes to checksum.

    Returns:
        int: The checksum.
    """
    crc = 0
    for byte in data:
        crc = crc ^ (byte << 8)
        for _ in range(8):
            if crc & 0x8000:
                crc = ((crc << 1) ^ 0x1021) & 0xFFFF
            else:
                crc = (crc << 1) & 0xFFFF
    return crc

GrowwNatsBuffer

GrowwNatsBuffer()

The NATS byte stream of one connection, buffered so that only whole lines and messages come out.

Starts with nothing buffered.

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/groww/GrowwNatsBuffer/example_1_control_lines_and_a_message.py
"""Feeds one websocket frame of NATS traffic to the buffer and prints the events it takes out.

NATS is line based: control lines end with CRLF, and a `MSG` line is followed by a payload of the length it declares. `GrowwNatsBuffer.feed` adds a websocket frame to the stream and returns, in order, an `info` event with the decoded INFO line, a `ping` event, an `error` event for an `-ERR` line, and a `message` event with the subject and payload of each whole `MSG`. Other control lines, such as `+OK` or `PONG`, produce nothing.

This program feeds one frame holding an INFO line, a PONG, a PING and a message. It needs no stand-ins.

Notice that the payload comes out as bytes exactly as long as the `MSG` line declared, even though it contains a CRLF of its own.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwNatsBuffer/example_1_control_lines_and_a_message.py
"""


from stock_brokers.websockets.groww import (
    GrowwNatsBuffer,
)


class ControlLinesAndAMessageExample:
    """Feeds one frame to a NATS buffer.

    Attributes:
        buffer (GrowwNatsBuffer): The buffer being shown.
    """

    def __init__(self):
        """Builds an empty buffer.

        Returns:
            None: This method returns nothing.
        """
        self.buffer = GrowwNatsBuffer()

    def run(self):
        """Feeds the frame and prints each event.

        Returns:
            None: This method returns nothing.
        """
        frame = 'INFO {"nonce": "example-nonce", "headers": true}\r\nPONG\r\nPING\r\nMSG /ld/eq/nse/book.2885 2 7\r\nab\r\ncde\r\n'
        for event in self.buffer.feed(frame):
            print(event)


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

It prints:

('info', {'nonce': 'example-nonce', 'headers': True})
('ping',)
('message', '/ld/eq/nse/book.2885', b'ab\r\ncde')
examples/stock_brokers/websockets/groww/GrowwNatsBuffer/example_2_split_across_frames.py
"""Feeds a NATS message split across three websocket frames and shows the buffer waiting until it is whole.

A websocket frame may end in the middle of a NATS line or payload, so `GrowwNatsBuffer` keeps what it cannot use yet and hands a message out only once its whole payload has arrived. Bytes and text frames are both accepted.

This program feeds a message in three pieces as bytes, the second ending inside the payload, and then an `-ERR` line as text. It needs no stand-ins.

Notice that the first two feeds return nothing, and that the third returns the whole message.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwNatsBuffer/example_2_split_across_frames.py
"""


from stock_brokers.websockets.groww import (
    GrowwNatsBuffer,
)


class SplitAcrossFramesExample:
    """Feeds a split message and an error line to a NATS buffer.

    Attributes:
        buffer (GrowwNatsBuffer): The buffer being shown.
    """

    def __init__(self):
        """Builds an empty buffer.

        Returns:
            None: This method returns nothing.
        """
        self.buffer = GrowwNatsBuffer()

    def run(self):
        """Feeds each piece and prints what each feed returned.

        Returns:
            None: This method returns nothing.
        """
        pieces = [
            b'MSG stocks/order/upd',
            b'ates.apex.subscription-77 1 10\r\n01234',
            b'56789\r\n',
            "-ERR 'Authorization Violation'\r\n",
        ]
        for piece in pieces:
            print(f'Fed {piece!r}: {self.buffer.feed(piece)}')


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

It prints:

Fed b'MSG stocks/order/upd': []
Fed b'ates.apex.subscription-77 1 10\r\n01234': []
Fed b'56789\r\n': [('message', 'stocks/order/updates.apex.subscription-77', b'0123456789')]
Fed "-ERR 'Authorization Violation'\r\n": [('error', "-ERR 'Authorization Violation'")]

Methods:

Name Description
feed

Adds a websocket frame to the stream and takes out everything it completed.

Source code in stock_brokers/websockets/groww.py
def __init__(self):
    """
    Starts with nothing buffered.

    Returns:
        None: This method returns nothing.
    """
    self._buffer = b""

feed

feed(message)

Adds a websocket frame to the stream and takes out everything it completed.

Parameters:

Name Type Description Default
message bytes | str

The frame.

required

Returns:

Type Description
list[tuple]

In order, ("info", info) for an INFO line, ("ping",) for a PING, ("error", line) for an -ERR line, and ("message", subject, payload) for each whole MSG.

Example usage

From example_1_control_lines_and_a_message.py
self.buffer.feed(frame)
From example_2_split_across_frames.py
print(f'Fed {piece!r}: {self.buffer.feed(piece)}')
Source code in stock_brokers/websockets/groww.py
def feed(self, message):
    """
    Adds a websocket frame to the stream and takes out everything it completed.

    Args:
        message (bytes | str): The frame.

    Returns:
        list[tuple]: In order, `("info", info)` for an INFO line, `("ping",)` for a PING, `("error", line)` for an `-ERR` line, and `("message", subject, payload)` for each whole `MSG`.
    """
    if isinstance(message, str):
        message = message.encode()
    self._buffer = self._buffer + bytes(message)
    events = []
    while True:
        end = self._buffer.find(b"\r\n")
        if end == -1:
            break
        line = self._buffer[:end]
        rest = self._buffer[end + 2:]
        if line.startswith(b"MSG "):
            parts = line.decode("utf-8", errors="replace").split()
            if len(parts) < 4:
                self._buffer = rest
                continue
            length = int(parts[-1])
            if len(rest) < length + 2:
                break
            events.append(("message", parts[1], rest[:length]))
            self._buffer = rest[length + 2:]
            continue

        self._buffer = rest
        if line.startswith(b"INFO "):
            events.append(("info", json.loads(line[5:].decode("utf-8"))))
        elif line == b"PING":
            events.append(("ping",))
        elif line.startswith(b"-ERR"):
            events.append(("error", line.decode("utf-8", errors="replace")))
    return events

GrowwSession

GrowwSession(logger)

The Groww login every socket authenticates with, logged in again by one socket at a time.

Constructs GrowwAPI, which logs in when the stored session is dead.

Parameters:

Name Type Description Default
logger Logger

Where logins are reported.

required

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever GrowwAPI raises when it cannot log in.

Example programs

examples/stock_brokers/websockets/groww/GrowwSession/example_1_socket_token_for_a_key_pair.py
"""Reads the Groww access token, exchanges a key pair for a socket token, and logs in again once.

`GrowwSession` holds one `GrowwAPI` and reads the access token afresh every time a socket connects. `socket_token_payload` asks Groww for a short-lived socket JWT and subscription id bound to a key pair's public key, authorized with that access token, and unwraps the answer's `payload`. `log_in_again` logs in only when the token a refused socket used is still the one in force.

Constructing the real `GrowwAPI` logs in to Groww, so the program registers a stand-in module under `stock_brokers.api.groww`, and a stand-in `requests` package answers the socket token request. The key pair is a real `GrowwNkeyPair`, freshly generated, so the program prints only the first letter of its public key.

Notice the bearer token in the request, the unwrapped payload, and that the second request with the stale token makes no login.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwSession/example_1_socket_token_for_a_key_pair.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwNkeyPair,
    GrowwSession,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class SocketTokenForAKeyPairExample:
    """Asks a Groww session for a socket token and logs in again around it.

    Attributes:
        session (GrowwSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-ins and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            200,
        ])
        self.session = GrowwSession(logging.getLogger('groww.session'))

    def run(self):
        """Prints the token, the socket token payload, and the token after two login requests.

        Returns:
            None: This method returns nothing.
        """
        failed_token = self.session.access_token()
        print(f'Access token: {failed_token}')
        payload = self.session.socket_token_payload(GrowwNkeyPair())
        print(f'Socket token payload: {payload}')
        self.session.log_in_again(failed_token)
        self.session.log_in_again(failed_token)
        print(f'Access token: {self.session.access_token()}')
        print(f'Logins made: {LoginRecord.login_count}')


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

It prints:

Stand-in login number 1
Access token: groww-token-1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Socket token payload: {'token': 'socket-jwt', 'subscriptionId': 'subscription-77'}
WARNING Logging in to Groww again.
Stand-in login number 2
Access token: groww-token-2
Logins made: 2
examples/stock_brokers/websockets/groww/GrowwSession/example_2_refused_then_unchecked_login.py
"""Shows a Groww session's socket token request refused, then a login made without checking and a retried request.

When the socket token endpoint answers HTTP 401 or 403, `GrowwSession.socket_token_payload` raises `GrowwSocketRefused`, because it means the access token is dead. The order update socket answers a refusal with `log_in_again_without_checking`, which logs in whether or not another process already replaced the token, and then asks again.

Constructing the real `GrowwAPI` logs in to Groww, so the program registers a stand-in module under `stock_brokers.api.groww`, and a stand-in `requests` package refuses the first socket token request with HTTP 401 and grants the second.

Notice the refusal message carrying the HTTP status and body, the second login, and that the retried request carries the new token.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwSession/example_2_refused_then_unchecked_login.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwNkeyPair,
    GrowwSession,
    GrowwSocketRefused,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class RefusedThenUncheckedLoginExample:
    """Recovers a Groww session from a refused socket token request.

    Attributes:
        session (GrowwSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-ins and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            401,
            200,
        ])
        self.session = GrowwSession(logging.getLogger('groww.session'))

    def run(self):
        """Requests a socket token, logs in again when refused, and requests again.

        Returns:
            None: This method returns nothing.
        """
        key_pair = GrowwNkeyPair()
        try:
            self.session.socket_token_payload(key_pair)
        except GrowwSocketRefused as refusal:
            print(f'Refused: {refusal}')
            self.session.log_in_again_without_checking()
        payload = self.session.socket_token_payload(key_pair)
        print(f'Socket token payload: {payload}')
        print(f'Logins made: {LoginRecord.login_count}')


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

It prints:

Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 401
Refused: HTTP 401: {"message": "Unauthorized"}
WARNING Logging in to Groww again.
Stand-in login number 2
Socket token request with Bearer groww-token-2 for a key starting U: HTTP 200
Socket token payload: {'token': 'socket-jwt', 'subscriptionId': 'subscription-77'}
Logins made: 2

Methods:

Name Description
access_token

The access token in force now, which may be one another process has just obtained.

socket_token_payload

Asks Groww for a socket JWT bound to a key pair's public key.

log_in_again

Logs in again, unless another socket or process already replaced the token that failed.

log_in_again_without_checking

Logs in again whether or not the token was already replaced.

Source code in stock_brokers/websockets/groww.py
def __init__(self, logger):
    """
    Constructs `GrowwAPI`, which logs in when the stored session is dead.

    Args:
        logger (logging.Logger): Where logins are reported.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `GrowwAPI` raises when it cannot log in.
    """
    from stock_brokers.api.groww import GrowwAPI

    self._api_class = GrowwAPI
    self._logger = logger
    self._lock = threading.Lock()
    self._groww = GrowwAPI()

access_token

access_token()

The access token in force now, which may be one another process has just obtained.

Returns:

Type Description
str | None

The token.

Example usage

From example_1_socket_token_for_a_key_pair.py
failed_token = self.session.access_token()
From example_1_socket_token_for_a_key_pair.py
print(f'Access token: {self.session.access_token()}')
Source code in stock_brokers/websockets/groww.py
def access_token(self):
    """
    The access token in force now, which may be one another process has just obtained.

    Returns:
        str | None: The token.
    """
    return (self._groww._current_login() or {}).get("access_token")

socket_token_payload

socket_token_payload(key_pair)

Asks Groww for a socket JWT bound to a key pair's public key.

Parameters:

Name Type Description Default
key_pair GrowwNkeyPair

The connection's key pair.

required

Returns:

Type Description
dict

The answer's payload, which should carry token and subscriptionId.

Raises:

Type Description
GrowwSocketRefused

For HTTP 401 or 403, which means the access token is dead.

HTTPError

For any other error status.

Example usage

From example_1_socket_token_for_a_key_pair.py
payload = self.session.socket_token_payload(GrowwNkeyPair())
From example_2_refused_then_unchecked_login.py
payload = self.session.socket_token_payload(key_pair)
From example_2_refused_then_unchecked_login.py
self.session.socket_token_payload(key_pair)
Source code in stock_brokers/websockets/groww.py
def socket_token_payload(self, key_pair):
    """
    Asks Groww for a socket JWT bound to a key pair's public key.

    Args:
        key_pair (GrowwNkeyPair): The connection's key pair.

    Returns:
        dict: The answer's payload, which should carry `token` and `subscriptionId`.

    Raises:
        GrowwSocketRefused: For HTTP 401 or 403, which means the access token is dead.
        requests.HTTPError: For any other error status.
    """
    import requests

    headers = {
        "x-request-id": str(uuid.uuid4()),
        "Authorization": f"Bearer {self.access_token()}",
        "Content-Type": "application/json",
        "x-client-id": "growwapi",
        "x-client-platform": "growwapi-python-client",
    }
    response = requests.post(SOCKET_TOKEN_URL, headers=headers, json={"socketKey": key_pair.public_key}, timeout=30)
    if response.status_code in (401, 403):
        raise GrowwSocketRefused(f"HTTP {response.status_code}: {response.text[:200]}")
    response.raise_for_status()
    payload = response.json()
    return payload.get("payload", payload)

log_in_again

log_in_again(stale_token)

Logs in again, unless another socket or process already replaced the token that failed.

Parameters:

Name Type Description Default
stale_token str | None

The access token the failing connection used.

required

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever GrowwAPI raises when it cannot log in.

Example usage

From example_1_socket_token_for_a_key_pair.py
self.session.log_in_again(failed_token)
Source code in stock_brokers/websockets/groww.py
def log_in_again(self, stale_token):
    """
    Logs in again, unless another socket or process already replaced the token that failed.

    Args:
        stale_token (str | None): The access token the failing connection used.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `GrowwAPI` raises when it cannot log in.
    """
    with self._lock:
        if self.access_token() != stale_token:
            return
        self._log_in()

log_in_again_without_checking

log_in_again_without_checking()

Logs in again whether or not the token was already replaced.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever GrowwAPI raises when it cannot log in.

Example usage

From example_2_refused_then_unchecked_login.py
self.session.log_in_again_without_checking()
Source code in stock_brokers/websockets/groww.py
def log_in_again_without_checking(self):
    """
    Logs in again whether or not the token was already replaced.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `GrowwAPI` raises when it cannot log in.
    """
    with self._lock:
        self._log_in()

_log_in

_log_in()

Logs in by constructing GrowwAPI again; the caller holds the lock.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever GrowwAPI raises when it cannot log in.

Source code in stock_brokers/websockets/groww.py
def _log_in(self):
    """
    Logs in by constructing `GrowwAPI` again; the caller holds the lock.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `GrowwAPI` raises when it cannot log in.
    """
    self._logger.warning("Logging in to Groww again.")
    self._groww = self._api_class()

GrowwQuotesSocket

GrowwQuotesSocket(name, tokens, names, session, on_ticks, logger)

Bases: BrokerWebsocket

One Groww NATS websocket carrying the price and depth of a batch of instruments.

Sets up a socket for one batch of instruments.

Parameters:

Name Type Description Default
name str

The connection's name, for the log.

required
tokens list[str]

The batch of EXCHANGE|SEGMENT|EXCHANGE_TOKEN instrument tokens.

required
names dict[str, str]

Each token to the instrument's name, shared between sockets; names the payloads reveal are added to it.

required
session GrowwSession

The shared login.

required
on_ticks callable

Called with each frame's list of ticks that carry a price or a book, on this socket's thread.

required
logger Logger

Where the connection reports.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/groww/GrowwQuotesSocket/example_1_price_and_depth_over_nats.py
"""Runs a Groww quotes socket through the socket token, the NATS handshake, and one price and one depth message.

`GrowwQuotesSocket` speaks NATS over a websocket. Before connecting it generates a fresh ed25519 key pair and exchanges its public key for a short-lived socket JWT. When the server opens with an INFO line carrying a nonce, the socket answers CONNECT with the JWT and the signed nonce, then PING, then one SUB for the price subject and one for the depth subject of each instrument. Payloads are protobuf `StocksSocketResponseProtoDto` messages; price and depth arrive separately and are merged per instrument, prices in paise are divided into rupees, and quantities arrive as doubles and are rounded.

This program subscribes NSE token 2885 and delivers a price message and a depth message in one websocket frame. No real socket or HTTP request is made: a stand-in `requests` package answers the socket token request, a stand-in `websocket` package plays the frames and prints the NATS lines the socket sends with the signature replaced by its length, because the key pair is fresh on every run, and a stand-in `stock_brokers.api.groww` module lets the real `GrowwSession` supply the access token without logging in. The payloads are built with `GrowwMessageClasses`.

Notice the two SUB lines, that the first tick is a quote and the second, after the depth message, is full with both, and that the instrument takes the name `NSE:RELIANCE` from the payload's symbol because the program gave it none.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwQuotesSocket/example_1_price_and_depth_over_nats.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwMessageClasses,
    GrowwQuotesSocket,
    GrowwSession,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class HandshakeRefused(Exception):
    """A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.

    Attributes:
        status_code (int): The HTTP status the broker answered with.
    """

    def __init__(self, status_code):
        """Keeps the status.

        Args:
            status_code (int): The HTTP status the broker answered with.

        Returns:
            None: This method returns nothing.
        """
        super().__init__(f'Handshake status {status_code}')
        self.status_code = status_code


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        plan (dict): Either a `refuse_with` status or a list of `frames`.
        handlers (dict): The socket's callbacks, by name.
        closed (bool): Whether the socket asked to close, after which no more frames are delivered.
    """

    def __init__(self, url, plan, handlers):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            plan (dict): Either a `refuse_with` status or a list of `frames`.
            handlers (dict): The socket's callbacks, by name.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.plan = plan
        self.handlers = handlers
        self.closed = False

    def run_forever(self, **options):
        """Plays the plan and returns when the pretend connection closes.

        Args:
            **options (dict): The ping and TLS settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        if 'refuse_with' in self.plan:
            self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
            self.handlers['on_close'](self, None, None)
            return
        self.handlers['on_open'](self)
        for frame in self.plan['frames']:
            if self.closed:
                break
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        text = data.rstrip('\r\n')
        if text.startswith('CONNECT '):
            connect = json.loads(text[len('CONNECT '):])
            connect['sig'] = f"<{len(connect['sig'])} character signature>"
            text = f'CONNECT {json.dumps(connect)}'
        print(f'Sent: {text}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class NatsFrames:
    """Builds the NATS lines Groww's server sends over the websocket."""

    def info(self):
        """The INFO line the server opens with, carrying a nonce.

        Returns:
            str: The line.
        """
        info = {
            'server_id': 'groww-nats',
            'nonce': 'example-nonce',
            'headers': True,
        }
        return f'INFO {json.dumps(info)}\r\n'

    def message(self, subject, payload):
        """A MSG line followed by its payload.

        Args:
            subject (str): The subject the message arrived on.
            payload (bytes): The protobuf payload.

        Returns:
            bytes: The message.
        """
        return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'


class PriceAndDepthOverNatsExample:
    """Runs a Groww quotes socket against one scripted NATS connection.

    Attributes:
        socket (GrowwQuotesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the payloads, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            200,
        ])
        response_class = GrowwMessageClasses().stocks_response_class()
        price = response_class()
        price.symbol = 'RELIANCE'
        price.stockLivePrice.tsInMillis = 1790311529000
        price.stockLivePrice.ltp = 295050
        price.stockLivePrice.open = 293000
        price.stockLivePrice.high = 296000
        price.stockLivePrice.low = 292500
        price.stockLivePrice.close = 291000
        price.stockLivePrice.volume = 1204500.0
        depth = response_class()
        depth.symbol = 'RELIANCE'
        depth.stocksMarketDepth.buyBook[1].price = 295000
        depth.stocksMarketDepth.buyBook[1].qty = 100.0
        depth.stocksMarketDepth.sellBook[1].price = 295100
        depth.stocksMarketDepth.sellBook[1].qty = 120.0
        nats = NatsFrames()
        frame = nats.message('/ld/eq/nse/price_detailed.2885', price.SerializeToString())
        frame = frame + nats.message('/ld/eq/nse/book.2885', depth.SerializeToString())
        plans = [
            {
                'frames': [
                    nats.info(),
                    frame,
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('groww.quotes')
        tokens = [
            'NSE|CASH|2885',
        ]
        self.names = {}
        self.socket = GrowwQuotesSocket('socket_0', tokens, self.names, GrowwSession(logger), self.print_ticks, logger)
        websocket_module.when_finished = self.socket.close

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the socket built.

        Args:
            ticks (list): The normalized ticks from one frame.

        Returns:
            None: This method returns nothing.
        """
        for tick in ticks:
            change = tick['change']
            if change is not None:
                change = round(change, 4)
            print(f"Tick {tick['id']} ({tick['instrument_token']}) in {tick['mode']} mode")
            print(f"  last_price={tick['last_price']} volume={tick['volume']} change={change} exchange_timestamp={tick['exchange_timestamp']}")
            print(f"  ohlc={tick['ohlc']}")
            print(f"  depth={tick['depth']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connection ends.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Names learned: {self.names}')
        print(f'Gave up: {self.socket.gave_up}')


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

It prints:

Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO socket_0 opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB /ld/eq/nse/price_detailed.2885 1
Sent: SUB /ld/eq/nse/book.2885 2
INFO socket_0 subscribed to 2 subject(s) for 1 instrument(s).
Tick NSE:RELIANCE (NSE|CASH|2885) in quote mode
  last_price=2950.5 volume=1204500 change=1.3918 exchange_timestamp=1790311529
  ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
  depth={'buy': [], 'sell': []}
Tick NSE:RELIANCE (NSE|CASH|2885) in full mode
  last_price=2950.5 volume=1204500 change=1.3918 exchange_timestamp=1790311529
  ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
  depth={'buy': [{'quantity': 100, 'price': 2950.0, 'orders': None}], 'sell': [{'quantity': 120, 'price': 2951.0, 'orders': None}]}
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Names learned: {'NSE|CASH|2885': 'NSE:RELIANCE'}
Gave up: False
examples/stock_brokers/websockets/groww/GrowwQuotesSocket/example_2_refused_socket_token.py
"""Shows a Groww quotes socket logging in again when its socket token is refused, then answering a server PING.

A dead Groww access token is discovered before any websocket opens: the socket token endpoint answers HTTP 401 or 403. `GrowwQuotesSocket` takes that as a refused login, and its reconnect loop asks the shared `GrowwSession` to log in again, skipping the login when another socket or process already replaced the token. Once connected, the socket answers a NATS `PING` from the server with `PONG`.

This program's first socket token request is refused with HTTP 403 and the second succeeds. The second connection opens with INFO, then the server sends a PING and an index value for the NIFTY index (token 26000 on the NSE cash segment). No real socket or HTTP request is made: stand-in `requests` and `websocket` packages play the answers and frames, and a stand-in `stock_brokers.api.groww` module counts each `GrowwAPI` construction as one login. The backoff is set to zero seconds.

Notice that the second token request carries the new access token, the PONG line, and that an index tick has a last price but no open, high, low or close.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwQuotesSocket/example_2_refused_socket_token.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwMessageClasses,
    GrowwQuotesSocket,
    GrowwSession,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class HandshakeRefused(Exception):
    """A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.

    Attributes:
        status_code (int): The HTTP status the broker answered with.
    """

    def __init__(self, status_code):
        """Keeps the status.

        Args:
            status_code (int): The HTTP status the broker answered with.

        Returns:
            None: This method returns nothing.
        """
        super().__init__(f'Handshake status {status_code}')
        self.status_code = status_code


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        plan (dict): Either a `refuse_with` status or a list of `frames`.
        handlers (dict): The socket's callbacks, by name.
        closed (bool): Whether the socket asked to close, after which no more frames are delivered.
    """

    def __init__(self, url, plan, handlers):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            plan (dict): Either a `refuse_with` status or a list of `frames`.
            handlers (dict): The socket's callbacks, by name.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.plan = plan
        self.handlers = handlers
        self.closed = False

    def run_forever(self, **options):
        """Plays the plan and returns when the pretend connection closes.

        Args:
            **options (dict): The ping and TLS settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        if 'refuse_with' in self.plan:
            self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
            self.handlers['on_close'](self, None, None)
            return
        self.handlers['on_open'](self)
        for frame in self.plan['frames']:
            if self.closed:
                break
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        text = data.rstrip('\r\n')
        if text.startswith('CONNECT '):
            connect = json.loads(text[len('CONNECT '):])
            connect['sig'] = f"<{len(connect['sig'])} character signature>"
            text = f'CONNECT {json.dumps(connect)}'
        print(f'Sent: {text}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class NatsFrames:
    """Builds the NATS lines Groww's server sends over the websocket."""

    def info(self):
        """The INFO line the server opens with, carrying a nonce.

        Returns:
            str: The line.
        """
        info = {
            'server_id': 'groww-nats',
            'nonce': 'example-nonce',
            'headers': True,
        }
        return f'INFO {json.dumps(info)}\r\n'

    def message(self, subject, payload):
        """A MSG line followed by its payload.

        Args:
            subject (str): The subject the message arrived on.
            payload (bytes): The protobuf payload.

        Returns:
            bytes: The message.
        """
        return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'


class RefusedSocketTokenExample:
    """Runs a Groww quotes socket through a refused token and a good connection.

    Attributes:
        socket (GrowwQuotesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the payload, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            403,
            200,
        ])
        index = GrowwMessageClasses().stocks_response_class()()
        index.stocksLiveIndices.value = 2515065
        index.stocksLiveIndices.tsInMillis = 1790311529000
        nats = NatsFrames()
        plans = [
            {
                'frames': [
                    nats.info(),
                    'PING\r\n',
                    nats.message('/ld/eq/nse/price_detailed.26000', index.SerializeToString()),
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('groww.quotes')
        tokens = [
            'NSE|CASH|26000',
        ]
        names = {
            'NSE|CASH|26000': 'NSE:NIFTY',
        }
        self.socket = GrowwQuotesSocket('socket_0', tokens, names, GrowwSession(logger), self.print_ticks, logger)
        self.socket.MIN_BACKOFF_SECONDS = 0
        websocket_module.when_finished = self.socket.close

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the socket built.

        Args:
            ticks (list): The normalized ticks from one frame.

        Returns:
            None: This method returns nothing.
        """
        for tick in ticks:
            change = tick['change']
            if change is not None:
                change = round(change, 4)
            print(f"Tick {tick['id']} ({tick['instrument_token']}) in {tick['mode']} mode")
            print(f"  last_price={tick['last_price']} volume={tick['volume']} change={change} exchange_timestamp={tick['exchange_timestamp']}")
            print(f"  ohlc={tick['ohlc']}")
            print(f"  depth={tick['depth']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connection ends.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')
        print(f'Logins made: {LoginRecord.login_count}')


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

It prints:

Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 403
ERROR socket_0 socket token refused: HTTP 403: {"message": "Unauthorized"}
WARNING Logging in to Groww again.
Stand-in login number 2
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
Socket token request with Bearer groww-token-2 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO socket_0 opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB /ld/eq/nse/price_detailed.26000 1
Sent: SUB /ld/eq/nse/book.26000 2
INFO socket_0 subscribed to 2 subject(s) for 1 instrument(s).
Sent: PONG
Tick NSE:NIFTY (NSE|CASH|26000) in quote mode
  last_price=25150.65 volume=None change=None exchange_timestamp=1790311529
  ohlc={'open': None, 'high': None, 'low': None, 'close': None}
  depth={'buy': [], 'sell': []}
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
Logins made: 2

Methods:

Name Description
close

Closes the connection and stops reconnecting.

run_forever

Connects, and reconnects with backoff, until closed or unable to recover.

Source code in stock_brokers/websockets/groww.py
def __init__(self, name, tokens, names, session, on_ticks, logger):
    """
    Sets up a socket for one batch of instruments.

    Args:
        name (str): The connection's name, for the log.
        tokens (list[str]): The batch of `EXCHANGE|SEGMENT|EXCHANGE_TOKEN` instrument tokens.
        names (dict[str, str]): Each token to the instrument's name, shared between sockets; names the payloads reveal are added to it.
        session (GrowwSession): The shared login.
        on_ticks (callable): Called with each frame's list of ticks that carry a price or a book, on this socket's thread.
        logger (logging.Logger): Where the connection reports.

    Returns:
        None: This method returns nothing.
    """
    super().__init__(name, logger)
    self._tokens = tokens
    self._names = names
    self._session = session
    self._on_ticks = on_ticks
    self._response_class = GrowwMessageClasses().stocks_response_class()
    self._token = None
    self._key_pair = None
    self._socket_jwt = None
    self._buffer = GrowwNatsBuffer()
    self._subjects = {}
    self._state = {}

_gives_up_after_logging_in_again

_gives_up_after_logging_in_again(failed_connects)

Gives up only once six connects in a row have failed, as Groww's quotes loop always has.

Parameters:

Name Type Description Default
failed_connects int

How many connects in a row have failed since the last login.

required

Returns:

Type Description
bool

True to give up now.

Source code in stock_brokers/websockets/groww.py
def _gives_up_after_logging_in_again(self, failed_connects):
    """
    Gives up only once six connects in a row have failed, as Groww's quotes loop always has.

    Args:
        failed_connects (int): How many connects in a row have failed since the last login.

    Returns:
        bool: True to give up now.
    """
    return failed_connects >= self.MAX_FAILED_CONNECTS

_connect

_connect()

Obtains a socket JWT for a fresh key pair, opens the websocket, and blocks until it closes.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
RuntimeError

When the socket token answer carries no token.

Source code in stock_brokers/websockets/groww.py
def _connect(self):
    """
    Obtains a socket JWT for a fresh key pair, opens the websocket, and blocks until it closes.

    Returns:
        None: This method returns nothing.

    Raises:
        RuntimeError: When the socket token answer carries no token.
    """
    import websocket

    self._token = self._session.access_token()
    self._key_pair = GrowwNkeyPair()
    try:
        payload = self._session.socket_token_payload(self._key_pair)
    except GrowwSocketRefused as exception:
        self._authentication_rejected = True
        self._logger.error(f"{self.name} socket token refused: {exception}")
        return
    if not payload.get("token"):
        raise RuntimeError(f"No token in the Groww socket token response: {str(payload)[:200]}")
    self._socket_jwt = payload["token"]
    self._buffer = GrowwNatsBuffer()
    self._subjects = {}
    self._state = {}
    self._websocket_application = websocket.WebSocketApp(
        FEED_URL,
        on_open=self._on_open,
        on_message=self._on_message,
        on_error=self._on_error,
        on_close=self._on_close,
    )
    self._websocket_application.run_forever(
        ping_interval=PING_INTERVAL_SECONDS,
        ping_timeout=PING_TIMEOUT_SECONDS,
    )

_log_in_again

_log_in_again()

Logs in again through the shared session, which skips the login when the token was already replaced.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _log_in_again(self):
    """
    Logs in again through the shared session, which skips the login when the token was already replaced.

    Returns:
        None: This method returns nothing.
    """
    self._session.log_in_again(self._token)

_on_open

_on_open(websocket_connection)

Notes the open connection; NATS speaks first, with INFO.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that opened.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_open(self, websocket_connection):
    """
    Notes the open connection; NATS speaks first, with INFO.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that opened.

    Returns:
        None: This method returns nothing.
    """
    self._opened = True
    self._logger.info(f"{self.name} opened. Waiting for the NATS INFO frame.")

_on_error

_on_error(websocket_connection, error)

Reports an error.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that failed.

required
error Exception

The error.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_error(self, websocket_connection, error):
    """
    Reports an error.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that failed.
        error (Exception): The error.

    Returns:
        None: This method returns nothing.
    """
    self._logger.error(f"{self.name} error: {error}")

_send

_send(lines)

Sends NATS lines, one websocket frame each.

Parameters:

Name Type Description Default
lines list[str]

The lines.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _send(self, lines):
    """
    Sends NATS lines, one websocket frame each.

    Args:
        lines (list[str]): The lines.

    Returns:
        None: This method returns nothing.
    """
    for line in lines:
        self._websocket_application.send(line)

_on_message

_on_message(websocket_connection, message)

Consumes a frame as NATS, answers its control lines, and hands on the ticks its messages completed.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection the frame arrived on.

required
message bytes | str

The frame.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_message(self, websocket_connection, message):
    """
    Consumes a frame as NATS, answers its control lines, and hands on the ticks its messages completed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection the frame arrived on.
        message (bytes | str): The frame.

    Returns:
        None: This method returns nothing.
    """
    delivered = []
    for event in self._buffer.feed(message):
        if event[0] == "info":
            self._authenticate(event[1])
        elif event[0] == "ping":
            self._send(["PONG\r\n"])
        elif event[0] == "error":
            self._on_nats_error(event[1])
        else:
            delivered.append((event[1], event[2]))

    ticks = []
    for subject, payload in delivered:
        token = self._subjects.get(subject)
        if token is None:
            continue
        try:
            state = self._apply_payload(token, payload)
        except Exception as exception:
            self._logger.warning(f"{self.name} could not decode a payload on {subject}: {exception}")
            continue
        tick = self._build_tick(token, state)
        if tick["last_price"] is not None or tick["depth"]["buy"] or tick["depth"]["sell"]:
            ticks.append(tick)
    if ticks:
        self._on_ticks(ticks)

_authenticate

_authenticate(info)

Answers INFO with CONNECT and the signed nonce, then subscribes the batch's price and depth subjects.

Parameters:

Name Type Description Default
info dict

The decoded INFO frame.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _authenticate(self, info):
    """
    Answers INFO with CONNECT and the signed nonce, then subscribes the batch's price and depth subjects.

    Args:
        info (dict): The decoded INFO frame.

    Returns:
        None: This method returns nothing.
    """
    connect = {
        "jwt": self._socket_jwt,
        "sig": self._key_pair.signed_nonce(info.get("nonce", "")),
        "verbose": False,
        "pedantic": False,
        "tls_required": False,
        "lang": "python",
        "version": "1.0.0",
        "protocol": 1,
        "headers": bool(info.get("headers", False)),
        "no_responders": False,
    }
    self._send([f"CONNECT {json.dumps(connect)}\r\n", "PING\r\n"])
    lines = []
    for token in self._tokens:
        exchange, segment, exchange_token = token.split("|")
        for prefixes in (PRICE_SUBJECTS, DEPTH_SUBJECTS):
            subject = f"{prefixes[(segment, exchange)]}{exchange_token}"
            self._subjects[subject] = token
            lines.append(f"SUB {subject} {len(self._subjects)}\r\n")
    self._send(lines)
    self._logger.info(f"{self.name} subscribed to {len(lines)} subject(s) for {len(self._tokens)} instrument(s).")

_on_nats_error

_on_nats_error(error)

Reports a NATS error, and closes the connection when it is an authorization failure.

Parameters:

Name Type Description Default
error str

The -ERR line.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_nats_error(self, error):
    """
    Reports a NATS error, and closes the connection when it is an authorization failure.

    Args:
        error (str): The `-ERR` line.

    Returns:
        None: This method returns nothing.
    """
    self._logger.error(f"{self.name} NATS error: {error}")
    for marker in AUTHENTICATION_ERRORS:
        if marker in error.lower():
            self._authentication_rejected = True
            self._websocket_application.close()
            return

_apply_payload

_apply_payload(token, payload)

Merges one protobuf payload into an instrument's state.

Parameters:

Name Type Description Default
token str

The instrument's EXCHANGE|SEGMENT|EXCHANGE_TOKEN.

required
payload bytes

The protobuf bytes.

required

Returns:

Type Description
dict

The instrument's merged state.

Raises:

Type Description
DecodeError

When the payload is not a StocksSocketResponseProtoDto.

Source code in stock_brokers/websockets/groww.py
def _apply_payload(self, token, payload):
    """
    Merges one protobuf payload into an instrument's state.

    Args:
        token (str): The instrument's `EXCHANGE|SEGMENT|EXCHANGE_TOKEN`.
        payload (bytes): The protobuf bytes.

    Returns:
        dict: The instrument's merged state.

    Raises:
        google.protobuf.message.DecodeError: When the payload is not a `StocksSocketResponseProtoDto`.
    """
    message = self._response_class.FromString(payload)
    state = self._state.setdefault(token, {})

    if message.HasField("stockLivePrice"):
        live = message.stockLivePrice
        for field in ("tsInMillis", "open", "high", "low", "close", "volume", "value", "bidQty", "offerQty", "avgPrice", "ltp", "openInterest"):
            if getattr(live, field, None):
                state[field] = getattr(live, field)

    if message.HasField("stocksMarketDepth"):
        book = message.stocksMarketDepth
        state["depth"] = {
            "buy": self._levels(book.buyBook),
            "sell": self._levels(book.sellBook),
        }
        if book.tsInMillis:
            state["tsInMillis"] = book.tsInMillis

    if message.HasField("stocksLiveIndices"):
        indices = message.stocksLiveIndices
        if indices.value:
            state["ltp"] = indices.value
        if indices.tsInMillis:
            state["tsInMillis"] = indices.tsInMillis

    if message.symbol and not self._names.get(token):
        self._names[token] = f"{token.split('|')[0]}:{message.symbol}"
    return state

_levels

_levels(book_map)

Depth levels from a protobuf book map keyed by level, read in level order.

Parameters:

Name Type Description Default
book_map MessageMap

The buy or sell book.

required

Returns:

Type Description
list[dict]

The levels.

Source code in stock_brokers/websockets/groww.py
def _levels(self, book_map):
    """
    Depth levels from a protobuf book map keyed by level, read in level order.

    Args:
        book_map (google.protobuf.internal.containers.MessageMap): The buy or sell book.

    Returns:
        list[dict]: The levels.
    """
    levels = []
    for _, entry in sorted(book_map.items()):
        levels.append({
            "quantity": self._whole(entry.qty),
            "price": entry.price / DIVISOR,
            "orders": None,
        })
    return levels

_price

_price(state, key)

A price from the merged state in rupees.

Parameters:

Name Type Description Default
state dict

The merged state.

required
key str

The field.

required

Returns:

Type Description
float | None

The price, or None when the field is missing.

Source code in stock_brokers/websockets/groww.py
def _price(self, state, key):
    """
    A price from the merged state in rupees.

    Args:
        state (dict): The merged state.
        key (str): The field.

    Returns:
        float | None: The price, or None when the field is missing.
    """
    if state.get(key) is None:
        return None
    return state[key] / DIVISOR

_whole

_whole(value)

A protobuf double quantity as a whole number.

Parameters:

Name Type Description Default
value float | None

The quantity.

required

Returns:

Type Description
int | None

The rounded quantity, or None.

Source code in stock_brokers/websockets/groww.py
def _whole(self, value):
    """
    A protobuf double quantity as a whole number.

    Args:
        value (float | None): The quantity.

    Returns:
        int | None: The rounded quantity, or None.
    """
    if value is None:
        return None
    return int(round(value))

_build_tick

_build_tick(token, state)

Builds a normalized tick from an instrument's merged state.

Parameters:

Name Type Description Default
token str

The instrument's EXCHANGE|SEGMENT|EXCHANGE_TOKEN.

required
state dict

The merged fields.

required

Returns:

Type Description
dict

The tick.

Source code in stock_brokers/websockets/groww.py
def _build_tick(self, token, state):
    """
    Builds a normalized tick from an instrument's merged state.

    Args:
        token (str): The instrument's `EXCHANGE|SEGMENT|EXCHANGE_TOKEN`.
        state (dict): The merged fields.

    Returns:
        dict: The tick.
    """
    last_price = self._price(state, "ltp")
    close = self._price(state, "close")
    mode = "quote"
    if state.get("depth"):
        mode = "full"
    change = None
    if last_price is not None and close:
        change = (last_price - close) * 100 / close
    exchange_timestamp = None
    if state.get("tsInMillis"):
        exchange_timestamp = int(state["tsInMillis"] / 1000)
    return {
        "id": self._names.get(token) or token,
        "broker": "groww",
        "instrument_token": token,
        "exchange": token.split("|")[0],
        "mode": mode,
        "last_price": last_price,
        "last_quantity": None,
        "average_price": self._price(state, "avgPrice"),
        "volume": self._whole(state.get("volume")),
        "buy_quantity": self._whole(state.get("bidQty")),
        "sell_quantity": self._whole(state.get("offerQty")),
        "ohlc": {
            "open": self._price(state, "open"),
            "high": self._price(state, "high"),
            "low": self._price(state, "low"),
            "close": close,
        },
        "change": change,
        "oi": self._whole(state.get("openInterest")),
        "oi_day_high": None,
        "oi_day_low": None,
        "last_trade_time": None,
        "exchange_timestamp": exchange_timestamp,
        "depth": state.get("depth") or {"buy": [], "sell": []},
        "received_at": time.time(),
    }

close

close()

Closes the connection and stops reconnecting.

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.close()
Source code in stock_brokers/websockets/base.py
def close(self):
    """
    Closes the connection and stops reconnecting.

    Returns:
        None: This method returns nothing.
    """
    self._stop.set()
    if self._websocket_application is not None:
        self._websocket_application.close()

run_forever

run_forever()

Connects, and reconnects with backoff, until closed or unable to recover.

Returns:

Type Description
None

This method returns nothing. gave_up says whether the socket stopped because it could not recover.

Example usage

From example_1_reconnecting_after_a_drop.py
self.socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.refused_socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.run_forever()
Source code in stock_brokers/websockets/base.py
def run_forever(self):
    """
    Connects, and reconnects with backoff, until closed or unable to recover.

    Returns:
        None: This method returns nothing. `gave_up` says whether the socket stopped because it could not recover.
    """
    backoff = self.MIN_BACKOFF_SECONDS
    failed_connects = 0
    logged_in_again = False
    while not self._stop.is_set():
        self._opened = False
        self._authentication_rejected = False
        try:
            self._connect()
        except Exception as exception:
            self._logger.error(f"{self.name} connection failed: {type(exception).__name__}: {exception}")
        if self._stop.is_set():
            break

        if self._opened and not self._authentication_rejected:
            failed_connects = 0
            logged_in_again = False
            backoff = self.MIN_BACKOFF_SECONDS
        else:
            failed_connects = failed_connects + 1

        if self._authentication_rejected or failed_connects >= self.MAX_FAILED_CONNECTS:
            if logged_in_again and self._gives_up_after_logging_in_again(failed_connects):
                self.gave_up = True
                self._logger.error(f"{self.name} still cannot connect after logging in again. Giving up.")
                break
            try:
                self._log_in_again()
            except Exception as exception:
                self.gave_up = True
                self._logger.error(f"{self.name} could not log in again: {type(exception).__name__}: {exception}")
                break
            failed_connects = 0
            logged_in_again = True

        self._logger.warning(f"{self.name} disconnected. Reconnecting in {backoff} seconds.")
        self._stop.wait(backoff)
        backoff = min(backoff * 2, self.MAX_BACKOFF_SECONDS)
    self._logger.info(f"{self.name} stopped.")

_on_close

_on_close(websocket_connection, status_code, message)

Reports that the connection closed.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that closed.

required
status_code int | None

The close status the broker sent.

required
message str | None

The close reason the broker sent.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/base.py
def _on_close(self, websocket_connection, status_code, message):
    """
    Reports that the connection closed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that closed.
        status_code (int | None): The close status the broker sent.
        message (str | None): The close reason the broker sent.

    Returns:
        None: This method returns nothing.
    """
    self._logger.info(f"{self.name} closed with status {status_code}: {message}")

GrowwOrderUpdatesSocket

GrowwOrderUpdatesSocket(session, on_updates, logger)

Bases: BrokerWebsocket

Groww's order and position update stream for the account, over NATS.

Sets up the order updates socket.

Parameters:

Name Type Description Default
session GrowwSession

The login.

required
on_updates callable

Called on this socket's thread with a frame's decoded orders, its decoded positions and the epoch the frame was received.

required
logger Logger

Where the connection reports.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/groww/GrowwOrderUpdatesSocket/example_1_orders_and_positions.py
"""Receives protobuf orders and positions on Groww's order update stream and prints the decoded dictionaries.

`GrowwOrderUpdatesSocket` obtains a socket JWT and a subscription id, answers the server's INFO with CONNECT, and subscribes the account's equity orders, derivatives orders and derivatives positions subjects, each keyed by the subscription id. Each payload is decoded to a plain dictionary with Groww's own field names, enum fields by name and every field present even when zero.

This program delivers an equity order, a derivatives position and a message on a subject the socket never subscribed, split across two websocket frames. No real socket or HTTP request is made: a stand-in `requests` package answers the socket token request, a stand-in `websocket` package plays the frames and prints the NATS lines the socket sends with the signature replaced by its length, and a stand-in `stock_brokers.api.groww` module lets the real `GrowwSession` supply the access token. The payloads are built with `GrowwMessageClasses`.

Notice the three SUB lines ending in `subscription-77`, that enums such as `orderStatus` arrive by name, and that the unexpected subject is logged and skipped.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwOrderUpdatesSocket/example_1_orders_and_positions.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwMessageClasses,
    GrowwOrderUpdatesSocket,
    GrowwSession,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class HandshakeRefused(Exception):
    """A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.

    Attributes:
        status_code (int): The HTTP status the broker answered with.
    """

    def __init__(self, status_code):
        """Keeps the status.

        Args:
            status_code (int): The HTTP status the broker answered with.

        Returns:
            None: This method returns nothing.
        """
        super().__init__(f'Handshake status {status_code}')
        self.status_code = status_code


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        plan (dict): Either a `refuse_with` status or a list of `frames`.
        handlers (dict): The socket's callbacks, by name.
        closed (bool): Whether the socket asked to close, after which no more frames are delivered.
    """

    def __init__(self, url, plan, handlers):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            plan (dict): Either a `refuse_with` status or a list of `frames`.
            handlers (dict): The socket's callbacks, by name.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.plan = plan
        self.handlers = handlers
        self.closed = False

    def run_forever(self, **options):
        """Plays the plan and returns when the pretend connection closes.

        Args:
            **options (dict): The ping and TLS settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        if 'refuse_with' in self.plan:
            self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
            self.handlers['on_close'](self, None, None)
            return
        self.handlers['on_open'](self)
        for frame in self.plan['frames']:
            if self.closed:
                break
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        text = data.rstrip('\r\n')
        if text.startswith('CONNECT '):
            connect = json.loads(text[len('CONNECT '):])
            connect['sig'] = f"<{len(connect['sig'])} character signature>"
            text = f'CONNECT {json.dumps(connect)}'
        print(f'Sent: {text}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class NatsFrames:
    """Builds the NATS lines Groww's server sends over the websocket."""

    def info(self):
        """The INFO line the server opens with, carrying a nonce.

        Returns:
            str: The line.
        """
        info = {
            'server_id': 'groww-nats',
            'nonce': 'example-nonce',
            'headers': True,
        }
        return f'INFO {json.dumps(info)}\r\n'

    def message(self, subject, payload):
        """A MSG line followed by its payload.

        Args:
            subject (str): The subject the message arrived on.
            payload (bytes): The protobuf payload.

        Returns:
            bytes: The message.
        """
        return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'


class OrdersAndPositionsExample:
    """Runs the Groww order update socket against one scripted NATS connection.

    Attributes:
        socket (GrowwOrderUpdatesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the payloads, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            200,
        ])
        position_class = GrowwMessageClasses().order_and_position_classes()[1]
        position = position_class()
        position.symbolData.contractId = 'NIFTY26SEP25000CE'
        position.symbolData.displayName = 'NIFTY 25000 Call 30 Sep'
        position.positionInfo.NSE.creditQty = 75
        position.positionInfo.NSE.creditPrice = 12550
        nats = NatsFrames()
        whole = nats.message('stocks/order/updates.apex.subscription-77', self.order_payload(11, 10))
        whole = whole + nats.message('stocks_fo/position/updates.apex.subscription-77', position.SerializeToString())
        stray = nats.message('stocks/order/updates.apex.someone-else', self.order_payload(1, 0))
        plans = [
            {
                'frames': [
                    nats.info(),
                    whole[:30],
                    whole[30:] + stray,
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('groww.orders')
        self.socket = GrowwOrderUpdatesSocket(GrowwSession(logger), self.print_updates, logger)
        websocket_module.when_finished = self.socket.close

    def order_payload(self, status, filled_quantity):
        """A serialized `OrderDetailsBroadCastDto` for an INFY buy.

        Args:
            status (int): Groww's order status number, 1 for acknowledged and 11 for completed.
            filled_quantity (int): How much has filled.

        Returns:
            bytes: The protobuf payload.
        """
        order_class = GrowwMessageClasses().order_and_position_classes()[0]
        broadcast = order_class()
        detail = broadcast.orderDetailUpdateDto
        detail.growwOrderId = 'GMK2609250001'
        detail.qty = 10
        detail.filledQty = filled_quantity
        detail.price = 150000
        detail.orderStatus = status
        detail.exchange = 1
        detail.buySell = 0
        return broadcast.SerializeToString()

    def print_updates(self, orders, positions, received_at):
        """Prints each decoded order and position the socket handed on.

        Args:
            orders (list): The frame's orders, as dictionaries with Groww's field names.
            positions (list): The frame's positions, as dictionaries with Groww's field names.
            received_at (float): The epoch the frame arrived, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        for order in orders:
            detail = order['orderDetailUpdateDto']
            print(f"Order {detail['growwOrderId']} {detail['orderStatus']}: {detail['buySell']} {detail['filledQty']} of {detail['qty']} on {detail['exchange']} at {detail['price']}")
        for position in positions:
            print(f"Position {position['symbolData']['contractId']}: NSE credit quantity {position['positionInfo']['NSE']['creditQty']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connection ends.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')


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

It prints:

Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO Order updates socket opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB stocks/order/updates.apex.subscription-77 1
Sent: SUB stocks_fo/order/updates.apex.subscription-77 2
Sent: SUB stocks_fo/position/updates.apex.subscription-77 3
INFO Authenticated and subscribed to 3 subject(s). Waiting for updates.
INFO Ignoring a message on an unexpected subject stocks/order/updates.apex.someone-else.
Order GMK2609250001 COMPLETED: B 10 of 10 on NSE at 150000
Position NIFTY26SEP25000CE: NSE credit quantity 75.0
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
examples/stock_brokers/websockets/groww/GrowwOrderUpdatesSocket/example_2_authorization_violation.py
"""Shows Groww's order update socket closing on a NATS authorization error and logging in again without checking.

When the NATS server answers with an `-ERR` naming an authorization failure, `GrowwOrderUpdatesSocket` closes the connection as a refused login, and its reconnect loop calls `GrowwSession.log_in_again_without_checking`, which logs in whether or not another process already replaced the token. The next connection asks for a new socket JWT with a new key pair.

This program's first connection answers CONNECT with `-ERR 'Authorization Violation'`; the second delivers one acknowledged order. No real socket or HTTP request is made: stand-in `requests` and `websocket` packages play the answers and frames, and a stand-in `stock_brokers.api.groww` module counts each `GrowwAPI` construction as one login. The backoff is set to zero seconds.

Notice the NATS error line, the second login, and that the second socket token request carries the new access token.

Run it from the project root:

    python examples/stock_brokers/websockets/groww/GrowwOrderUpdatesSocket/example_2_authorization_violation.py
"""

import json
import logging
import sys
import types

from stock_brokers.websockets.groww import (
    GrowwMessageClasses,
    GrowwOrderUpdatesSocket,
    GrowwSession,
)


class LoginRecord:
    """The Groww login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        access_token (str | None): The token in force now.
        login_count (int): How many logins the stand-in API has made.
    """

    access_token = None
    login_count = 0


class StandInGrowwAPI:
    """A stand-in for `GrowwAPI` whose construction is a pretend login."""

    def __init__(self):
        """Logs in, which here only issues the next numbered token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.access_token = f'groww-token-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self._settings = {
            'api_key': 'groww-api-key',
        }

    def _current_login(self):
        """The login in force now, as the real class reads it from Redis.

        Returns:
            dict | None: The access token, or None before any login.
        """
        if LoginRecord.access_token is None:
            return None
        return {
            'access_token': LoginRecord.access_token,
        }


class SocketTokenResponse:
    """A stand-in for the `requests` response of Groww's socket token endpoint.

    Attributes:
        status_code (int): The HTTP status.
        body (dict): The JSON body.
        text (str): The body as text.
    """

    def __init__(self, status_code, body):
        """Keeps the answer.

        Args:
            status_code (int): The HTTP status.
            body (dict): The JSON body.

        Returns:
            None: This method returns nothing.
        """
        self.status_code = status_code
        self.body = body
        self.text = json.dumps(body)

    def json(self):
        """The JSON body.

        Returns:
            dict: The body.
        """
        return self.body

    def raise_for_status(self):
        """Does nothing, because every scripted answer that reaches it succeeded.

        Returns:
            None: This method returns nothing.
        """


class StandInRequests:
    """A stand-in for the `requests` package that answers socket token requests in turn.

    Attributes:
        statuses (list): The HTTP status of each answer, in order.
    """

    def __init__(self, statuses):
        """Keeps the scripted statuses.

        Args:
            statuses (list): The HTTP status of each answer, in order.

        Returns:
            None: This method returns nothing.
        """
        self.statuses = statuses

    def post(self, url, headers, json, timeout):
        """Answers one socket token request.

        Args:
            url (str): The endpoint.
            headers (dict): The request headers, carrying the access token.
            json (dict): The request body with the public key.
            timeout (int): The request timeout in seconds.

        Returns:
            SocketTokenResponse: The answer.
        """
        status = self.statuses.pop(0)
        print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
        if status != 200:
            body = {
                'message': 'Unauthorized',
            }
            return SocketTokenResponse(status, body)
        body = {
            'payload': {
                'token': 'socket-jwt',
                'subscriptionId': 'subscription-77',
            },
        }
        return SocketTokenResponse(status, body)


class HandshakeRefused(Exception):
    """A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.

    Attributes:
        status_code (int): The HTTP status the broker answered with.
    """

    def __init__(self, status_code):
        """Keeps the status.

        Args:
            status_code (int): The HTTP status the broker answered with.

        Returns:
            None: This method returns nothing.
        """
        super().__init__(f'Handshake status {status_code}')
        self.status_code = status_code


class ScriptedConnection:
    """A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.

    Attributes:
        url (str): The URL the socket asked for.
        plan (dict): Either a `refuse_with` status or a list of `frames`.
        handlers (dict): The socket's callbacks, by name.
        closed (bool): Whether the socket asked to close, after which no more frames are delivered.
    """

    def __init__(self, url, plan, handlers):
        """Keeps what the socket passed in.

        Args:
            url (str): The URL the socket asked for.
            plan (dict): Either a `refuse_with` status or a list of `frames`.
            handlers (dict): The socket's callbacks, by name.

        Returns:
            None: This method returns nothing.
        """
        self.url = url
        self.plan = plan
        self.handlers = handlers
        self.closed = False

    def run_forever(self, **options):
        """Plays the plan and returns when the pretend connection closes.

        Args:
            **options (dict): The ping and TLS settings the socket asks for.

        Returns:
            None: This method returns nothing.
        """
        print(f'Connecting to {self.url}')
        if 'refuse_with' in self.plan:
            self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
            self.handlers['on_close'](self, None, None)
            return
        self.handlers['on_open'](self)
        for frame in self.plan['frames']:
            if self.closed:
                break
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        text = data.rstrip('\r\n')
        if text.startswith('CONNECT '):
            connect = json.loads(text[len('CONNECT '):])
            connect['sig'] = f"<{len(connect['sig'])} character signature>"
            text = f'CONNECT {json.dumps(connect)}'
        print(f'Sent: {text}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class NatsFrames:
    """Builds the NATS lines Groww's server sends over the websocket."""

    def info(self):
        """The INFO line the server opens with, carrying a nonce.

        Returns:
            str: The line.
        """
        info = {
            'server_id': 'groww-nats',
            'nonce': 'example-nonce',
            'headers': True,
        }
        return f'INFO {json.dumps(info)}\r\n'

    def message(self, subject, payload):
        """A MSG line followed by its payload.

        Args:
            subject (str): The subject the message arrived on.
            payload (bytes): The protobuf payload.

        Returns:
            bytes: The message.
        """
        return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'


class AuthorizationViolationExample:
    """Runs the Groww order update socket through an authorization error and a good connection.

    Attributes:
        socket (GrowwOrderUpdatesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the payloads, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.groww')
        api_module.GrowwAPI = StandInGrowwAPI
        sys.modules['stock_brokers.api.groww'] = api_module
        sys.modules['requests'] = StandInRequests([
            200,
            200,
        ])
        nats = NatsFrames()
        plans = [
            {
                'frames': [
                    nats.info(),
                    "-ERR 'Authorization Violation'\r\n",
                ],
            },
            {
                'frames': [
                    nats.info(),
                    nats.message('stocks/order/updates.apex.subscription-77', self.order_payload(1, 0)),
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('groww.orders')
        self.socket = GrowwOrderUpdatesSocket(GrowwSession(logger), self.print_updates, logger)
        self.socket.MIN_BACKOFF_SECONDS = 0
        websocket_module.when_finished = self.socket.close

    def order_payload(self, status, filled_quantity):
        """A serialized `OrderDetailsBroadCastDto` for an INFY buy.

        Args:
            status (int): Groww's order status number, 1 for acknowledged and 11 for completed.
            filled_quantity (int): How much has filled.

        Returns:
            bytes: The protobuf payload.
        """
        order_class = GrowwMessageClasses().order_and_position_classes()[0]
        broadcast = order_class()
        detail = broadcast.orderDetailUpdateDto
        detail.growwOrderId = 'GMK2609250001'
        detail.qty = 10
        detail.filledQty = filled_quantity
        detail.price = 150000
        detail.orderStatus = status
        detail.exchange = 1
        detail.buySell = 0
        return broadcast.SerializeToString()

    def print_updates(self, orders, positions, received_at):
        """Prints each decoded order and position the socket handed on.

        Args:
            orders (list): The frame's orders, as dictionaries with Groww's field names.
            positions (list): The frame's positions, as dictionaries with Groww's field names.
            received_at (float): The epoch the frame arrived, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        for order in orders:
            detail = order['orderDetailUpdateDto']
            print(f"Order {detail['growwOrderId']} {detail['orderStatus']}: {detail['buySell']} {detail['filledQty']} of {detail['qty']} on {detail['exchange']} at {detail['price']}")
        for position in positions:
            print(f"Position {position['symbolData']['contractId']}: NSE credit quantity {position['positionInfo']['NSE']['creditQty']}")

    def run(self):
        """Runs the socket's reconnect loop until the scripted connections run out.

        Returns:
            None: This method returns nothing.
        """
        self.socket.run_forever()
        print(f'Gave up: {self.socket.gave_up}')
        print(f'Logins made: {LoginRecord.login_count}')


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

It prints:

Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO Order updates socket opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB stocks/order/updates.apex.subscription-77 1
Sent: SUB stocks_fo/order/updates.apex.subscription-77 2
Sent: SUB stocks_fo/position/updates.apex.subscription-77 3
INFO Authenticated and subscribed to 3 subject(s). Waiting for updates.
ERROR NATS error: -ERR 'Authorization Violation'
INFO Order updates socket closed with status 1000: normal closure
WARNING Logging in to Groww again.
Stand-in login number 2
WARNING Order updates socket disconnected. Reconnecting in 0 seconds.
Socket token request with Bearer groww-token-2 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO Order updates socket opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB stocks/order/updates.apex.subscription-77 1
Sent: SUB stocks_fo/order/updates.apex.subscription-77 2
Sent: SUB stocks_fo/position/updates.apex.subscription-77 3
INFO Authenticated and subscribed to 3 subject(s). Waiting for updates.
Order GMK2609250001 ACKED: B 0 of 10 on NSE at 150000
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
Logins made: 2

Methods:

Name Description
close

Closes the connection and stops reconnecting.

run_forever

Connects, and reconnects with backoff, until closed or unable to recover.

Source code in stock_brokers/websockets/groww.py
def __init__(self, session, on_updates, logger):
    """
    Sets up the order updates socket.

    Args:
        session (GrowwSession): The login.
        on_updates (callable): Called on this socket's thread with a frame's decoded orders, its decoded positions and the epoch the frame was received.
        logger (logging.Logger): Where the connection reports.

    Returns:
        None: This method returns nothing.
    """
    super().__init__("Order updates socket", logger)
    self._session = session
    self._on_updates = on_updates
    self._message_classes = GrowwMessageClasses()
    self._order_class, self._position_class = self._message_classes.order_and_position_classes()
    self._key_pair = None
    self._socket_jwt = None
    self._subscription_id = None
    self._buffer = GrowwNatsBuffer()

_connect

_connect()

Obtains a socket JWT and subscription id for a fresh key pair, opens the websocket, and blocks until it closes.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
RuntimeError

When the socket token answer carries no token or no subscription id.

Source code in stock_brokers/websockets/groww.py
def _connect(self):
    """
    Obtains a socket JWT and subscription id for a fresh key pair, opens the websocket, and blocks until it closes.

    Returns:
        None: This method returns nothing.

    Raises:
        RuntimeError: When the socket token answer carries no token or no subscription id.
    """
    import websocket

    self._key_pair = GrowwNkeyPair()
    try:
        payload = self._session.socket_token_payload(self._key_pair)
    except GrowwSocketRefused as exception:
        self._authentication_rejected = True
        self._logger.error(f"Socket token refused: {exception}")
        return
    if not payload.get("token") or not payload.get("subscriptionId"):
        raise RuntimeError(f"No token or subscription id in the Groww socket token response: {str(payload)[:200]}")
    self._socket_jwt = payload["token"]
    self._subscription_id = payload["subscriptionId"]
    self._buffer = GrowwNatsBuffer()
    self._websocket_application = websocket.WebSocketApp(
        FEED_URL,
        on_open=self._on_open,
        on_message=self._on_message,
        on_error=self._on_error,
        on_close=self._on_close,
    )
    self._websocket_application.run_forever(
        ping_interval=PING_INTERVAL_SECONDS,
        ping_timeout=PING_TIMEOUT_SECONDS,
    )

_log_in_again

_log_in_again()

Logs in again, without checking whether another process already replaced the token.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _log_in_again(self):
    """
    Logs in again, without checking whether another process already replaced the token.

    Returns:
        None: This method returns nothing.
    """
    self._session.log_in_again_without_checking()

_on_open

_on_open(websocket_connection)

Notes the open connection; NATS speaks first, with INFO.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that opened.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_open(self, websocket_connection):
    """
    Notes the open connection; NATS speaks first, with INFO.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that opened.

    Returns:
        None: This method returns nothing.
    """
    self._opened = True
    self._logger.info(f"{self.name} opened. Waiting for the NATS INFO frame.")

_on_error

_on_error(websocket_connection, error)

Reports an error.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that failed.

required
error Exception

The error.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_error(self, websocket_connection, error):
    """
    Reports an error.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that failed.
        error (Exception): The error.

    Returns:
        None: This method returns nothing.
    """
    self._logger.error(f"Order updates error: {error}")

_order_subjects

_order_subjects()

The subjects carrying this account's equity and derivatives orders.

Returns:

Type Description
list[str]

The subjects.

Source code in stock_brokers/websockets/groww.py
def _order_subjects(self):
    """
    The subjects carrying this account's equity and derivatives orders.

    Returns:
        list[str]: The subjects.
    """
    return [
        f"{EQUITY_ORDERS}{self._subscription_id}",
        f"{DERIVATIVES_ORDERS}{self._subscription_id}",
    ]

_position_subjects

_position_subjects()

The subject carrying this account's derivatives positions.

Returns:

Type Description
list[str]

The subjects.

Source code in stock_brokers/websockets/groww.py
def _position_subjects(self):
    """
    The subject carrying this account's derivatives positions.

    Returns:
        list[str]: The subjects.
    """
    return [
        f"{DERIVATIVES_POSITIONS}{self._subscription_id}",
    ]

_on_message

_on_message(websocket_connection, message)

Consumes a frame as NATS, answers its control lines, and hands on the orders and positions its messages carried.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection the frame arrived on.

required
message bytes | str

The frame.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_message(self, websocket_connection, message):
    """
    Consumes a frame as NATS, answers its control lines, and hands on the orders and positions its messages carried.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection the frame arrived on.
        message (bytes | str): The frame.

    Returns:
        None: This method returns nothing.
    """
    received_at = datetime.now().timestamp()
    delivered = []
    for event in self._buffer.feed(message):
        if event[0] == "info":
            self._authenticate(event[1])
        elif event[0] == "ping":
            self._websocket_application.send("PONG\r\n")
        elif event[0] == "error":
            self._on_nats_error(event[1])
        else:
            delivered.append((event[1], event[2]))

    order_subjects = set(self._order_subjects())
    position_subjects = set(self._position_subjects())
    orders = []
    positions = []
    for subject, payload in delivered:
        try:
            if subject in order_subjects:
                broadcast = self._order_class.FromString(payload)
                if broadcast.HasField("orderDetailUpdateDto") and broadcast.orderDetailUpdateDto.growwOrderId:
                    orders.append(self._message_classes.as_dict(broadcast))
            elif subject in position_subjects:
                detail = self._position_class.FromString(payload)
                if detail.HasField("symbolData"):
                    positions.append(self._message_classes.as_dict(detail))
            else:
                self._logger.info(f"Ignoring a message on an unexpected subject {subject}.")
        except Exception as exception:
            self._logger.warning(f"Could not decode a payload on {subject}: {type(exception).__name__}: {exception}")
    if orders or positions:
        self._on_updates(orders, positions, received_at)

_authenticate

_authenticate(info)

Answers INFO with CONNECT and the signed nonce, then subscribes the account's order and position subjects.

Parameters:

Name Type Description Default
info dict

The decoded INFO frame.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _authenticate(self, info):
    """
    Answers INFO with CONNECT and the signed nonce, then subscribes the account's order and position subjects.

    Args:
        info (dict): The decoded INFO frame.

    Returns:
        None: This method returns nothing.
    """
    connect = {
        "jwt": self._socket_jwt,
        "sig": self._key_pair.signed_nonce(info.get("nonce", "")),
        "verbose": False,
        "pedantic": False,
        "tls_required": False,
        "lang": "python",
        "version": "1.0.0",
        "protocol": 1,
        "headers": bool(info.get("headers", False)),
        "no_responders": False,
    }
    subjects = self._order_subjects() + self._position_subjects()
    lines = [
        f"CONNECT {json.dumps(connect)}\r\n",
        "PING\r\n",
    ]
    index = 1
    for subject in subjects:
        lines.append(f"SUB {subject} {index}\r\n")
        index = index + 1
    for line in lines:
        self._websocket_application.send(line)
    self._logger.info(f"Authenticated and subscribed to {len(subjects)} subject(s). Waiting for updates.")

_on_nats_error

_on_nats_error(error)

Reports a NATS error, and closes the connection when it is an authorization failure.

Parameters:

Name Type Description Default
error str

The -ERR line.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/groww.py
def _on_nats_error(self, error):
    """
    Reports a NATS error, and closes the connection when it is an authorization failure.

    Args:
        error (str): The `-ERR` line.

    Returns:
        None: This method returns nothing.
    """
    self._logger.error(f"NATS error: {error}")
    for marker in AUTHENTICATION_ERRORS:
        if marker in error.lower():
            self._authentication_rejected = True
            self._websocket_application.close()
            return

close

close()

Closes the connection and stops reconnecting.

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.close()
Source code in stock_brokers/websockets/base.py
def close(self):
    """
    Closes the connection and stops reconnecting.

    Returns:
        None: This method returns nothing.
    """
    self._stop.set()
    if self._websocket_application is not None:
        self._websocket_application.close()

run_forever

run_forever()

Connects, and reconnects with backoff, until closed or unable to recover.

Returns:

Type Description
None

This method returns nothing. gave_up says whether the socket stopped because it could not recover.

Example usage

From example_1_reconnecting_after_a_drop.py
self.socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.refused_socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.run_forever()
Source code in stock_brokers/websockets/base.py
def run_forever(self):
    """
    Connects, and reconnects with backoff, until closed or unable to recover.

    Returns:
        None: This method returns nothing. `gave_up` says whether the socket stopped because it could not recover.
    """
    backoff = self.MIN_BACKOFF_SECONDS
    failed_connects = 0
    logged_in_again = False
    while not self._stop.is_set():
        self._opened = False
        self._authentication_rejected = False
        try:
            self._connect()
        except Exception as exception:
            self._logger.error(f"{self.name} connection failed: {type(exception).__name__}: {exception}")
        if self._stop.is_set():
            break

        if self._opened and not self._authentication_rejected:
            failed_connects = 0
            logged_in_again = False
            backoff = self.MIN_BACKOFF_SECONDS
        else:
            failed_connects = failed_connects + 1

        if self._authentication_rejected or failed_connects >= self.MAX_FAILED_CONNECTS:
            if logged_in_again and self._gives_up_after_logging_in_again(failed_connects):
                self.gave_up = True
                self._logger.error(f"{self.name} still cannot connect after logging in again. Giving up.")
                break
            try:
                self._log_in_again()
            except Exception as exception:
                self.gave_up = True
                self._logger.error(f"{self.name} could not log in again: {type(exception).__name__}: {exception}")
                break
            failed_connects = 0
            logged_in_again = True

        self._logger.warning(f"{self.name} disconnected. Reconnecting in {backoff} seconds.")
        self._stop.wait(backoff)
        backoff = min(backoff * 2, self.MAX_BACKOFF_SECONDS)
    self._logger.info(f"{self.name} stopped.")

_gives_up_after_logging_in_again

_gives_up_after_logging_in_again(failed_connects)

Whether a socket that has already logged in again gives up on this failure.

The loop asks only when the connection was refused or has failed six times in a row. By default the answer is yes, so a refusal straight after logging in again ends the socket. A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.

Parameters:

Name Type Description Default
failed_connects int

How many connects in a row have failed since the last login.

required

Returns:

Type Description
bool

True to give up now.

Source code in stock_brokers/websockets/base.py
def _gives_up_after_logging_in_again(self, failed_connects):
    """
    Whether a socket that has already logged in again gives up on this failure.

    The loop asks only when the connection was refused or has failed six times in a row.
    By default the answer is yes, so a refusal straight after logging in again ends the socket.
    A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.

    Args:
        failed_connects (int): How many connects in a row have failed since the last login.

    Returns:
        bool: True to give up now.
    """
    return True

_on_close

_on_close(websocket_connection, status_code, message)

Reports that the connection closed.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection that closed.

required
status_code int | None

The close status the broker sent.

required
message str | None

The close reason the broker sent.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/base.py
def _on_close(self, websocket_connection, status_code, message):
    """
    Reports that the connection closed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that closed.
        status_code (int | None): The close status the broker sent.
        message (str | None): The close reason the broker sent.

    Returns:
        None: This method returns nothing.
    """
    self._logger.info(f"{self.name} closed with status {status_code}: {message}")