indmoney
¶
IND Money instrument master ingestion.
IND Money is the only broker here whose master is not public. It serves three CSV files behind an Authorization header carrying an access token, which lasts twenty-four hours.
The token is read from the MongoDB last_login collection, where the broker login records it, so this module keeps no second copy of the token in the environment. A token issued on an earlier day is rejected rather than sent, because IND Money would only answer it with an authentication error that says nothing about the cause. When no usable token is stored, the ingester logs in itself through ensure_session rather than failing: the daily instrument download runs before the morning session refresh, so depending on that ordering would mean failing every weekday.
Classes:
| Name | Description |
|---|---|
IndMoneyInstruments |
Downloads IND Money's daily instrument master. |
Functions:
| Name | Description |
|---|---|
stored_access_token |
Read today's IND Money access token from the MongoDB last_login collection. |
IndMoneyInstruments
¶
Bases: BrokerInstruments
Downloads IND Money's daily instrument master.
Attributes:
| Name | Type | Description |
|---|---|---|
BROKER_NAME |
str
|
Always "indmoney". |
DEDUPE_KEY_COLUMNS |
list[str]
|
Exchange, segment and security identifier together. |
DEDUPE_SORT_COLUMN |
str | None
|
Series, so the kept row is predictable. |
Build the IND Money ingester.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
access_token
|
str | None
|
Token to authenticate with. When omitted, today's token is read from the MongoDB last_login collection. |
None
|
Example programs
"""Downloads IND Money's three instrument master files with an access token, served from memory instead of the network.
IND Money is the one broker here whose master is not public. `IndMoneyInstruments.download` asks the same endpoint three times, once for each `source` (`equity`, `fno` and `index`), with the access token in the `Authorization` header, reads each CSV answer as text and stacks them. The token is normally read from MongoDB when the ingester is built; passing `access_token` to the constructor, as this program does, uses that token instead and reads nothing.
The program must not download anything, so `requests.get` is replaced, only while `download` runs, by `InstrumentFileServer`, a stand-in that answers each source with a few recorded-looking lines and remembers what each request carried. Building the ingester creates a database engine, but the engine opens no connection until a query runs, and this program runs none.
Notice that all three requests carry the token given to the constructor, and that each source's rows follow the previous one's in the stacked frame.
Run it from the project root:
python examples/stock_brokers/instruments/indmoney/IndMoneyInstruments/example_1_downloading_with_a_stored_token.py
"""
import unittest.mock
import requests
from stock_brokers.instruments.indmoney import (
IndMoneyInstruments,
)
SOURCE_FILES = {
'equity': (
'exch,segment,security_id,instrument_name,trading_symbol,lot_units,tick_size,series,symbol_name,isin\n'
'NSE,E,1594,EQUITY,INFY,1,0.1,EQ,INFOSYS LIMITED,INE009A01021\n'
'BSE,E,500209,EQUITY,INFY,1,0.05,A,INFOSYS LTD,INE009A01021\n'
),
'fno': (
'exch,segment,security_id,instrument_name,trading_symbol,lot_units,tick_size,series,symbol_name,isin\n'
'NSE,D,52175,FUTIDX,NIFTY-Oct2026-FUT,75,0.1,,NIFTY,\n'
),
'index': (
'exch,segment,security_id,instrument_name,trading_symbol,lot_units,tick_size,series,symbol_name,isin\n'
'NSE,I,13,INDEX,NIFTY,1,0.05,,NIFTY 50,\n'
),
}
class InstrumentFileServer:
"""A stand-in for `requests.get` that answers each IND Money source with a canned CSV file.
Attributes:
files (dict): Each source mapped to the text of its CSV file.
requests_made (list): The `source` parameter and `Authorization` header of every request, in order.
"""
def __init__(self, files):
"""Holds the canned files.
Args:
files (dict): Each source mapped to the text of its CSV file.
Returns:
None: This method returns nothing.
"""
self.files = files
self.requests_made = []
def get(self, url, params=None, headers=None, timeout=None):
"""Answers one source's request, in place of `requests.get`.
Args:
url (str): The instruments endpoint.
params (dict | None): The query parameters, naming the source.
headers (dict | None): The request headers, carrying the token.
timeout (float | None): The timeout, which the stand-in ignores.
Returns:
requests.Response: A successful answer carrying the source's CSV file.
"""
source = params['source']
self.requests_made.append((source, headers['Authorization']))
response = requests.Response()
response.status_code = 200
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
response.encoding = 'utf-8'
response._content = self.files[source].encode('utf-8')
response.url = url
return response
class DownloadingWithAStoredTokenExample:
"""Downloads the master with a given token and prints the requests and the rows.
Attributes:
server (InstrumentFileServer): The stand-in for IND Money's endpoint.
instruments (IndMoneyInstruments): The ingester being shown.
"""
def __init__(self):
"""Builds the stand-in server and the ingester with a token.
Returns:
None: This method returns nothing.
"""
self.server = InstrumentFileServer(SOURCE_FILES)
self.instruments = IndMoneyInstruments(access_token='indmoney-token-issued-today')
def run(self):
"""Downloads the master and prints what each request carried and the rows returned.
Returns:
None: This method returns nothing.
"""
with unittest.mock.patch.object(requests, 'get', self.server.get):
frame = self.instruments.download()
for source, authorization in self.server.requests_made:
print(f'Requested source={source} with Authorization: {authorization}')
print(f'Rows: {len(frame)}')
print(frame.to_string())
if __name__ == '__main__':
DownloadingWithAStoredTokenExample().run()
It prints:
Requested source=equity with Authorization: indmoney-token-issued-today
Requested source=fno with Authorization: indmoney-token-issued-today
Requested source=index with Authorization: indmoney-token-issued-today
Rows: 4
exch segment security_id instrument_name trading_symbol lot_units tick_size series symbol_name isin
0 NSE E 1594 EQUITY INFY 1 0.1 EQ INFOSYS LIMITED INE009A01021
1 BSE E 500209 EQUITY INFY 1 0.05 A INFOSYS LTD INE009A01021
2 NSE D 52175 FUTIDX NIFTY-Oct2026-FUT 75 0.1 NaN NIFTY NaN
3 NSE I 13 INDEX NIFTY 1 0.05 NaN NIFTY 50 NaN
"""Logs IND Money in with `establish_session` when the only stored token is from an earlier day, then downloads with the new one.
IND Money's tokens last twenty-four hours, so when the ingester is built without a token it reads the stored login from MongoDB and keeps the token only if it was issued today. The daily download runs before the morning login refresh, so on most weekdays the stored token is yesterday's and nothing is kept. `establish_session` then logs IND Money in through `ensure_session`, which shares a Redis lock and a retry limit with every other process logging brokers in, and returns the token the login stored. `download` does this itself whenever it has no token; the program calls it directly to show its result.
Nothing here may reach MongoDB, Redis or IND Money, so for the length of the program the MongoDB and Redis clients of the session module and the instruments module are replaced by small stand-ins, the session module's `api_class_for` is replaced so that constructing "the IND Money API class" calls `StandInLogin.log_in`, which stores a new login dated today, and `requests.get` is replaced by a stand-in that answers each source with a canned CSV file.
Notice that the ingester starts with no token although one is stored, that one login produces the token, and that all three downloads carry it.
Run it from the project root:
python examples/stock_brokers/instruments/indmoney/IndMoneyInstruments/example_2_logging_in_when_no_token_is_stored.py
"""
import datetime
import unittest.mock
import requests
from stock_brokers.api.utilities import session as session_module
from stock_brokers.instruments import indmoney as indmoney_instruments
from stock_brokers.instruments.indmoney import (
IndMoneyInstruments,
)
CSV_FILE = (
'exch,segment,security_id,instrument_name,trading_symbol,lot_units,tick_size,series,symbol_name,isin\n'
'NSE,E,1594,EQUITY,INFY,1,0.1,EQ,INFOSYS LIMITED,INE009A01021\n'
)
class LoginStore:
"""A stand-in for MongoDB and its `last_login` collection, holding one login per broker.
Attributes:
logins (dict): Each broker name mapped to its stored login document.
"""
def __init__(self, logins):
"""Holds the stored logins.
Args:
logins (dict): Each broker name mapped to its stored login document.
Returns:
None: This method returns nothing.
"""
self.logins = logins
def __getitem__(self, name):
"""Returns the store itself as the `last_login` collection, as `database['last_login']` does.
Args:
name (str): The collection name, always `last_login` here.
Returns:
LoginStore: This store.
"""
return self
def find_one(self, query, projection=None):
"""Finds the stored login of the broker the query names.
Args:
query (dict): The fields to match, here `broker_name`.
projection (dict | None): The fields to return, which the stand-in ignores.
Returns:
dict | None: A copy of the stored login, or None when there is none.
"""
login = self.logins.get(query['broker_name'])
if login is None:
return None
return dict(login)
class StandInRedis:
"""A dictionary-backed stand-in for the Redis commands `ensure_session` uses.
Attributes:
keys (dict): Each key mapped to its value.
"""
def __init__(self):
"""Starts with nothing stored.
Returns:
None: This method returns nothing.
"""
self.keys = {}
def get(self, name):
"""Reads a key.
Args:
name (str): The key.
Returns:
str | None: The value, or None when the key is absent.
"""
return self.keys.get(name)
def set(self, name, value, nx=False, ex=None):
"""Sets a key, only when it is absent if `nx` is given.
Args:
name (str): The key.
value (str): The value to store.
nx (bool): True to set the key only when it does not exist.
ex (int | None): The expiry in seconds, which the stand-in ignores.
Returns:
bool: True when the key was set.
"""
if nx and name in self.keys:
return False
self.keys[name] = value
return True
def delete(self, name):
"""Removes a key.
Args:
name (str): The key.
Returns:
int: 1 when the key existed, otherwise 0.
"""
if name in self.keys:
del self.keys[name]
return 1
return 0
class StandInLogin:
"""A stand-in for constructing `INDMoneyAPI`, whose construction is the login.
Attributes:
store (LoginStore): Where the login is stored, as the real login writes MongoDB.
logins_made (int): How many logins have been made.
"""
def __init__(self, store):
"""Holds the store the login writes to.
Args:
store (LoginStore): Where the login is stored.
Returns:
None: This method returns nothing.
"""
self.store = store
self.logins_made = 0
def log_in(self):
"""Stores a new IND Money login dated now, as a successful real login does.
Returns:
None: This method returns nothing.
"""
self.logins_made = self.logins_made + 1
self.store.logins['indmoney'] = {
'broker_name': 'indmoney',
'access_token': 'indmoney-token-from-login',
'last_login': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
class CsvServer:
"""A stand-in for `requests.get` that answers every source with the same canned CSV file.
Attributes:
authorizations (list): The `Authorization` header of every request, in order.
"""
def __init__(self):
"""Starts with no requests seen.
Returns:
None: This method returns nothing.
"""
self.authorizations = []
def get(self, url, params=None, headers=None, timeout=None):
"""Answers one source's request, in place of `requests.get`.
Args:
url (str): The instruments endpoint.
params (dict | None): The query parameters, naming the source.
headers (dict | None): The request headers, carrying the token.
timeout (float | None): The timeout, which the stand-in ignores.
Returns:
requests.Response: A successful answer carrying the canned file.
"""
self.authorizations.append(headers['Authorization'])
response = requests.Response()
response.status_code = 200
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
response.encoding = 'utf-8'
response._content = CSV_FILE.encode('utf-8')
response.url = url
return response
class LoggingInWhenNoTokenIsStoredExample:
"""Builds the ingester with only yesterday's token stored, logs in, and downloads.
Attributes:
store (LoginStore): The stand-in for MongoDB, holding yesterday's login.
login (StandInLogin): The stand-in for the IND Money login.
server (CsvServer): The stand-in for IND Money's endpoint.
"""
def __init__(self):
"""Builds the stand-ins.
Returns:
None: This method returns nothing.
"""
self.store = LoginStore({
'indmoney': {
'broker_name': 'indmoney',
'access_token': 'indmoney-token-from-yesterday',
'last_login': '2026-09-29 07:00:04',
},
})
self.login = StandInLogin(self.store)
self.server = CsvServer()
def run(self):
"""Builds the ingester, establishes a session, downloads, and prints each step's result.
Returns:
None: This method returns nothing.
"""
with (
unittest.mock.patch.object(indmoney_instruments, 'get_mongo_db', return_value=self.store),
unittest.mock.patch.object(session_module, 'get_mongo_db', return_value=self.store),
unittest.mock.patch.object(session_module, 'get_cache', return_value=StandInRedis()),
unittest.mock.patch.object(session_module, 'api_class_for', return_value=self.login.log_in),
unittest.mock.patch.object(requests, 'get', self.server.get),
):
instruments = IndMoneyInstruments()
print(f'Token kept at construction: {instruments.access_token}')
instruments.access_token = instruments.establish_session()
print(f'Token after establish_session: {instruments.access_token}')
print(f'Logins made: {self.login.logins_made}')
frame = instruments.download()
print(f'Download requests carried: {self.server.authorizations}')
print(f'Rows: {len(frame)}')
if __name__ == '__main__':
LoggingInWhenNoTokenIsStoredExample().run()
It prints:
"""Shows `IndMoneyInstruments.download` refusing to run when no token is stored and logging in fails.
When the ingester has no token from today, `download` calls `establish_session` to log IND Money in. If that login fails, `establish_session` prints why and returns None instead of raising, and `download` then raises a `ValueError` that says where to look, before sending any request, because IND Money would only answer an unauthenticated request with an error that says nothing about the cause.
Nothing here may reach MongoDB, Redis or IND Money, so for the length of the program the MongoDB and Redis clients of the session module and the instruments module are replaced by small stand-ins, and the session module's `api_class_for` is replaced so that constructing "the IND Money API class" calls `RefusedLogin.log_in`, which fails the way a rejected TOTP does. `requests.get` is replaced by a stand-in that counts requests, to show that none is sent.
Notice the line `establish_session` prints about the failed login, the `ValueError` that follows it, and that no download request was made.
Run it from the project root:
python examples/stock_brokers/instruments/indmoney/IndMoneyInstruments/example_3_when_no_token_can_be_had.py
"""
import unittest.mock
import requests
from stock_brokers.api.indmoney import (
INDMoneyAPIException,
)
from stock_brokers.api.utilities import session as session_module
from stock_brokers.instruments import indmoney as indmoney_instruments
from stock_brokers.instruments.indmoney import (
IndMoneyInstruments,
)
class LoginStore:
"""A stand-in for MongoDB and its `last_login` collection, holding one login per broker.
Attributes:
logins (dict): Each broker name mapped to its stored login document.
"""
def __init__(self, logins):
"""Holds the stored logins.
Args:
logins (dict): Each broker name mapped to its stored login document.
Returns:
None: This method returns nothing.
"""
self.logins = logins
def __getitem__(self, name):
"""Returns the store itself as the `last_login` collection, as `database['last_login']` does.
Args:
name (str): The collection name, always `last_login` here.
Returns:
LoginStore: This store.
"""
return self
def find_one(self, query, projection=None):
"""Finds the stored login of the broker the query names.
Args:
query (dict): The fields to match, here `broker_name`.
projection (dict | None): The fields to return, which the stand-in ignores.
Returns:
dict | None: A copy of the stored login, or None when there is none.
"""
login = self.logins.get(query['broker_name'])
if login is None:
return None
return dict(login)
class StandInRedis:
"""A dictionary-backed stand-in for the Redis commands `ensure_session` uses.
Attributes:
keys (dict): Each key mapped to its value.
"""
def __init__(self):
"""Starts with nothing stored.
Returns:
None: This method returns nothing.
"""
self.keys = {}
def get(self, name):
"""Reads a key.
Args:
name (str): The key.
Returns:
str | None: The value, or None when the key is absent.
"""
return self.keys.get(name)
def set(self, name, value, nx=False, ex=None):
"""Sets a key, only when it is absent if `nx` is given.
Args:
name (str): The key.
value (str): The value to store.
nx (bool): True to set the key only when it does not exist.
ex (int | None): The expiry in seconds, which the stand-in ignores.
Returns:
bool: True when the key was set.
"""
if nx and name in self.keys:
return False
self.keys[name] = value
return True
def delete(self, name):
"""Removes a key.
Args:
name (str): The key.
Returns:
int: 1 when the key existed, otherwise 0.
"""
if name in self.keys:
del self.keys[name]
return 1
return 0
class RefusedLogin:
"""A stand-in for constructing `INDMoneyAPI`, whose login IND Money refuses."""
def log_in(self):
"""Fails the login as IND Money does for a TOTP it rejects.
Returns:
None: This method never returns.
Raises:
INDMoneyAPIException: Always.
"""
raise INDMoneyAPIException(code=401, message='Cannot generate access token from INDstocks API: invalid TOTP')
class CountingServer:
"""A stand-in for `requests.get` that only counts the requests it is sent.
Attributes:
requests_made (int): How many requests were sent.
"""
def __init__(self):
"""Starts with no requests seen.
Returns:
None: This method returns nothing.
"""
self.requests_made = 0
def get(self, url, params=None, headers=None, timeout=None):
"""Counts one request and refuses it, in place of `requests.get`.
Args:
url (str): The instruments endpoint.
params (dict | None): The query parameters.
headers (dict | None): The request headers.
timeout (float | None): The timeout, which the stand-in ignores.
Returns:
requests.Response: Never returns.
Raises:
requests.ConnectionError: Always, since no request should be sent.
"""
self.requests_made = self.requests_made + 1
raise requests.ConnectionError('No request should have been sent.')
class WhenNoTokenCanBeHadExample:
"""Runs `download` with only yesterday's token stored and a login that fails.
Attributes:
store (LoginStore): The stand-in for MongoDB, holding yesterday's login.
server (CountingServer): The stand-in for IND Money's endpoint.
"""
def __init__(self):
"""Builds the stand-ins.
Returns:
None: This method returns nothing.
"""
self.store = LoginStore({
'indmoney': {
'broker_name': 'indmoney',
'access_token': 'indmoney-token-from-yesterday',
'last_login': '2026-09-29 07:00:04',
},
})
self.server = CountingServer()
def run(self):
"""Builds the ingester, runs `download` and prints what it raised.
Returns:
None: This method returns nothing.
"""
with (
unittest.mock.patch.object(indmoney_instruments, 'get_mongo_db', return_value=self.store),
unittest.mock.patch.object(session_module, 'get_mongo_db', return_value=self.store),
unittest.mock.patch.object(session_module, 'get_cache', return_value=StandInRedis()),
unittest.mock.patch.object(session_module, 'api_class_for', return_value=RefusedLogin().log_in),
unittest.mock.patch.object(requests, 'get', self.server.get),
):
instruments = IndMoneyInstruments()
try:
instruments.download()
except ValueError as error:
print(f'Raised ValueError: {error}')
print(f'Download requests sent: {self.server.requests_made}')
if __name__ == '__main__':
WhenNoTokenCanBeHadExample().run()
It prints:
indmoney: could not establish a session for the instrument download: Could not establish a indmoney session: INDMoneyAPIException: (401, 'Cannot generate access token from INDstocks API: invalid TOTP')
Raised ValueError: No IND Money access token available, and logging in did not produce one. Try 'bin/indmoney/session/connect' and read the error it reports. The token lasts twenty-four hours, and one issued on an earlier day is not reused.
Download requests sent: 0
Methods:
| Name | Description |
|---|---|
establish_session |
Log IND Money in and return the token that produces, or None. |
download |
Fetch IND Money's three instrument master CSV files. |
normalize_columns |
Rewrite column names into a stable lowercase form usable as a SQL identifier. |
strip_whitespace |
Remove leading and trailing whitespace from every text column. |
drop_unnamed_columns |
Drop the empty placeholder columns a trailing delimiter on every line creates. |
drop_garbage_rows |
Drop the placeholder instruments some brokers ship in their master files. |
dedupe |
Drop rows repeating this broker's natural key. |
table_columns |
The column names of this broker's table as it currently exists in the database. |
has_data_for |
Whether this broker's table already holds a snapshot for a given date. |
ingest |
Download, clean and store one day's instrument master for this broker. |
check_row_count_deviation |
Compare a day's row count against the average of every earlier day. |
Source code in stock_brokers/instruments/indmoney.py
establish_session
¶
Log IND Money in and return the token that produces, or None.
Called only when no usable token is already stored. Every other broker publishes its instrument master as a public file, so this is the one ingester whose success depends on a session, and the daily download runs before the morning login refresh - without this it would fail every weekday on a stale token and wait for somebody to notice.
Goes through ensure_session rather than constructing the API class directly, so it
shares the Redis lock and the rate limiter with any other process using ensure_session.
IND Money issues one token per session, so a download logging in while another process does
the same would otherwise leave one of them holding a replaced token.
Returns:
| Type | Description |
|---|---|
str | None
|
Today's token, or None when the login did not produce one. |
Example usage
instruments.access_token = instruments.establish_session()
Source code in stock_brokers/instruments/indmoney.py
download
¶
Fetch IND Money's three instrument master CSV files.
Logs in first when no usable token is stored, since the endpoint rejects an unauthenticated request and a token issued on an earlier day is treated as absent.
Returns:
| Type | Description |
|---|---|
DataFrame
|
Every row IND Money published, read as text. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no access token could be obtained. |
HTTPError
|
If any of the three requests fails. |
Example usage
Source code in stock_brokers/instruments/indmoney.py
normalize_columns
¶
Rewrite column names into a stable lowercase form usable as a SQL identifier.
Some brokers bake stray whitespace and punctuation into the header itself rather than it being a parsing artifact, so the raw name is not safe to key on.
frameis the frame whose column names are to be normalized.
Example usage
frame = self.instruments.normalize_columns(self.instruments.download()).astype(object)
Source code in stock_brokers/instruments/base.py
strip_whitespace
¶
Remove leading and trailing whitespace from every text column.
Brokers intermittently pad text fields, so the same value can arrive padded one day and bare the next.
frameis the frame to strip.
Example usage
Source code in stock_brokers/instruments/base.py
drop_unnamed_columns
¶
Drop the empty placeholder columns a trailing delimiter on every line creates.
A column is only dropped when it is empty on every row. One carrying real values is kept and reported instead, so nothing is discarded silently.
frameis the frame to examine.
Example usage
Source code in stock_brokers/instruments/base.py
drop_garbage_rows
¶
Drop the placeholder instruments some brokers ship in their master files.
frameis the frame to filter.
Example usage
Source code in stock_brokers/instruments/base.py
dedupe
¶
Drop rows repeating this broker's natural key.
frameis the frame to de-duplicate.
Example usage
Source code in stock_brokers/instruments/base.py
table_columns
¶
The column names of this broker's table as it currently exists in the database.
The live table is the single source of truth for its own shape: the DDL lives in
stock_brokers/instruments/sql/ddl and nothing in Python restates it.
Example usage
print(f'Columns of {self.instruments.table}: {self.instruments.table_columns()}')
Source code in stock_brokers/instruments/base.py
has_data_for
¶
Whether this broker's table already holds a snapshot for a given date.
download_dateis the snapshot date to look for.
Example usage
print(f'Stored for {self.download_date} before: {self.instruments.has_data_for(self.download_date)}')
print(f'Stored for {self.download_date} after: {self.instruments.has_data_for(self.download_date)}')
Source code in stock_brokers/instruments/base.py
ingest
¶
Download, clean and store one day's instrument master for this broker.
Re-running for a date already stored is a no-op unless bootstrap is set, so the daily
job is safe to run twice.
download_dateis the snapshot date to record, defaulting to today.bootstrapreplaces that date's stored rows rather than skipping.
Example usage
stored = self.instruments.ingest(download_date=self.download_date)
stored = self.instruments.ingest(download_date=self.download_date, bootstrap=True)
Source code in stock_brokers/instruments/base.py
check_row_count_deviation
¶
Compare a day's row count against the average of every earlier day.
A large swing usually means the broker's file came back truncated or unexpectedly bloated. This reports and never raises: a deviation is a signal to investigate, not a failure.
download_dateis the snapshot date to check.thresholdis the allowed fractional deviation, so 0.10 permits ten percent either way.
Example usage
report = self.instruments.check_row_count_deviation(self.download_date)
report = self.instruments.check_row_count_deviation(self.download_date, threshold=0.5)
report = self.instruments.check_row_count_deviation(datetime.date(2026, 10, 1))
Source code in stock_brokers/instruments/base.py
stored_access_token
¶
Read today's IND Money access token from the MongoDB last_login collection.
The daily broker login job writes one document per broker there, holding the token it received and the moment it received it. A token from an earlier day is not returned: IND Money's tokens last twenty-four hours, and sending a stale one produces an authentication error that says nothing about why.
Returns:
| Type | Description |
|---|---|
str | None
|
The token issued today, or None when there is no document, no token in it, or the token was issued on an earlier date. |