Skip to content

Commit 50089ad

Browse files
committed
added article detail to explain authentication code.
1 parent 19e9dd1 commit 50089ad

3 files changed

Lines changed: 297 additions & 146 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,5 @@ pyrightconfig.json
179179
.venv/
180180

181181
src/example_async_taskgroup.py
182+
183+
async_call_nb_walkthrough.md

Article.md

Lines changed: 259 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,74 +17,281 @@ This project demonstrates how to use [`httpx`](https://www.python-httpx.org/) to
1717

1818
**Note**: A basic knowledge of Python [built-in asyncio](https://docs.python.org/3/library/asyncio.html) library is required to understand example codes.
1919

20-
## Included Notebook
20+
## What is Data Platform APIs?
2121

22-
### `src/sync_call_nb.ipynb` — Synchronous, step-by-step Jupyter notebook
22+
[LSEG Data Platform](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis) (RDP APIs, also known as Delivery Platform in LSEG Real-Time) provides simple web based API access to a broad range of LSEG content.
2323

24-
Interactive notebook version of the synchronous workflow. Each logical step is a separate cell with a markdown explanation above it, making it easy to run and inspect results incrementally.
24+
RDP APIs give developers seamless and holistic access to all of the LSEG content such as Historical Pricing, Environmental Social and Governance (ESG), News, Research, etc, and commingled with their content, enriching, integrating, and distributing the data through a single interface, delivered wherever they need it. The RDP APIs delivery mechanisms are the following:
25+
* Request - Response: RESTful web service (HTTP GET, POST, PUT or DELETE)
26+
* Alert: delivery is a mechanism to receive asynchronous updates (alerts) to a subscription.
27+
* Bulks: deliver substantial payloads, like the end-of-day pricing data for the whole venue.
28+
* Streaming: deliver real-time delivery of messages.
2529

26-
Demonstrates:
27-
- `POST /auth/oauth2/v1/token` — OAuth 2.0 Password Grant authentication
28-
- `GET /data/historical-pricing/v1/views/interday-summaries/{ric}` — daily OHLCV data with corporate-action adjustments for 10 RICs
29-
- `POST /auth/oauth2/v1/revoke` — session token revocation using HTTP Basic Auth
30-
- Shared `httpx.Client` inside a `with` block for clean connection-pool teardown
31-
- Wall-clock timing across the full workflow
30+
This example project is focusing on the Request-Response: RESTful web service delivery method only.
3231

33-
Notebook structure:
34-
1. Imports
35-
2. Constants (endpoint paths, RIC list)
36-
3. Credentials loaded from `src/.env`
37-
4. Helper functions (`post_authentication`, `post_auth_revoke`, `get_historical_interday_summaries`)
38-
5. Main execution block — authenticate, fetch data sequentially, revoke token
39-
6. Elapsed time output
32+
For more detail regarding the Data Platform, please see the following APIs resources:
33+
- [Quick Start](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis/quick-start) page.
34+
- [Tutorials](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis/tutorials) page.
35+
- [RDP APIs: Introduction to the Request-Response API](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis/tutorials#introduction-to-the-request-response-api) page.
4036

41-
### `src/async_call_nb.ipynb` — Async, concurrent Jupyter notebook (`asyncio.gather`)
37+
## What is HTTPX?
4238

43-
Interactive notebook version of the async concurrent workflow using `httpx.AsyncClient` and `asyncio.gather()`. Jupyter's native top-level `await` support means no `asyncio.run()` wrapper is needed.
39+
[HTTPX](https://www.python-httpx.org/) is a full featured modern HTTP client for Python 3. It provides a set of synchronous and modern asynchronous APIs with [HTTP/2](https://httpwg.org/specs/rfc7540.html) supported. It is largely [compatible with the Requests library](https://www.python-httpx.org/compatibility/), so any Python developers can migrate their existing [Requests](https://requests.readthedocs.io/en/latest/) library code to the HTTPX easily.
4440

45-
Demonstrates:
46-
- `POST /auth/oauth2/v1/token` — async OAuth 2.0 Password Grant authentication
47-
- `GET /data/historical-pricing/v1/views/interday-summaries/{ric}` — daily OHLCV data fetched concurrently for 10 RICs
48-
- `asyncio.Semaphore` — caps concurrent in-flight requests (default: 3) to respect server rate limits
49-
- `asyncio.gather(return_exceptions=True)` — all RIC coroutines run simultaneously; one failure does not cancel the rest
50-
- Per-result error inspection: `httpx.HTTPStatusError`, `httpx.RequestError`, generic `Exception`
51-
- `async with httpx.AsyncClient` — shared connection pool, closed cleanly on exit
52-
- Wall-clock timing across the full workflow
41+
```python
42+
import httpx
5343

54-
Notebook structure:
55-
1. Imports
56-
2. Constants (endpoint paths, RIC list)
57-
3. Credentials loaded from `src/.env`
58-
4. Helper functions (`post_authentication`, `post_auth_revoke`, `get_historical_interday_summaries`)
59-
5. Main execution block — authenticate, gather concurrent RIC fetches, per-result error handling
60-
6. Elapsed time output
44+
# Get
45+
params = {'key1': 'value1', 'key2': 'value2'}
46+
r = httpx.get('https://httpbin.org/get', params=params)
47+
r.raise_for_status()
48+
print(r.json())
6149

62-
## Included Scripts
50+
# HTTP Post
51+
data = {'integer': 123, 'boolean': True, 'list': ['a', 'b', 'c']}
52+
r = httpx.post('https://httpbin.org/post', json=data)
53+
r.raise_for_status()
54+
print(r.json())
55+
```
6356

64-
### `src/example_async_gather.py` — Async with `asyncio.gather()` and `Semaphore`
57+
For synchronous use, HTTPX also provides [`httpx.Client`](https://www.python-httpx.org/advanced/clients/) object which is the equivalent of `requests.Session()` — it maintains a shared connection pool across multiple requests:
6558

66-
Async script that fires all RIC requests concurrently via `asyncio.gather()`, with an `asyncio.Semaphore` to cap the number of in-flight requests and avoid hitting server rate limits.
59+
Example:
6760

68-
Demonstrates:
69-
- `POST /auth/oauth2/v1/token` — async authentication
70-
- `GET /data/historical-pricing/v1/views/interday-summaries/{ric}` — concurrent fetches for 10 RICs
71-
- `asyncio.Semaphore` — limits concurrent requests (default: 3)
72-
- `return_exceptions=True` — prevents one failure from cancelling the rest; each result is inspected individually
73-
- Per-result error handling: `httpx.HTTPStatusError`, `httpx.RequestError`, generic `Exception`
61+
```python
62+
import httpx
7463

75-
### `src/example_client.py` — Synchronous with shared client
64+
with httpx.Client(base_url='http://httpbin.org') as client:
65+
r = client.get('/get')
66+
r.raise_for_status()
67+
print(r.status_code)
68+
```
7669

77-
Synchronous (blocking) script using a single shared `httpx.Client` instance for connection pooling and consistent configuration across all requests.
70+
For asynchronous use, [`httpx.AsyncClient`](https://www.python-httpx.org/api/#asyncclient) works with [asyncio](https://docs.python.org/3/library/asyncio.html), [Trio](https://trio.readthedocs.io/en/stable/), and [AnyIO](https://anyio.readthedocs.io/en/stable/). I am demonstrating with asyncio in this project.:
71+
72+
Example:
73+
74+
```python
75+
import asyncio
76+
import httpx
77+
78+
async def main():
79+
async with httpx.AsyncClient() as client:
80+
response = await client.get('https://www.example.com/')
81+
print(response)
82+
83+
asyncio.run(main())
84+
```
85+
86+
## What are Synchronous and Asynchronous Execution Models?
87+
88+
**Synchronous** code runs tasks one at a time in a strict sequence — each task must finish before the next one starts. The application pauses and waits at every blocking call. For example, the `httpx.get()` function call below (equivalent to `requests.get()`) blocks the entire program until the HTTP response arrives:
89+
90+
```python
91+
import httpx
92+
93+
def fetch(url):
94+
"""Fetch the content of the URL synchronously."""
95+
r = httpx.get(url, verify=False)
96+
print("Fetched:", url, "status:", r.status_code)
97+
return r.text
98+
99+
def main():
100+
""" Main function."""
101+
fetch("https://example.org")
102+
print("This line prints ONLY after the request is done!")
103+
104+
if __name__ == "__main__":
105+
main()
106+
```
107+
108+
![synchronous code result](images/01_httpx_sync.png)
109+
110+
If the HTTP request takes 60 seconds, the program idles for those 60 seconds before executing the next line. For a single request this is fine, but it becomes a bottleneck when you need to fetch data for many symbols or endpoints.
111+
112+
![synchronous](images/synchronous_simple.png)
113+
114+
On the other hand, **Asynchronous** code allows multiple tasks to run concurrently in a non-blocking manner. While one task is waiting for I/O (such as a network response), the event loop can hand control to another task (execute next line of codes) instead of sitting idle. The example below uses `asyncio.create_task()` to launch a fetch in the background and immediately continues to the next line — without waiting for the response:
115+
116+
```python
117+
import asyncio
118+
import httpx
119+
120+
async def fetch(url):
121+
"""Fetch the content of the URL asynchronously."""
122+
async with httpx.AsyncClient(verify=False) as client:
123+
r = await client.get(url)
124+
print("Fetched:", url, "status:", r.status_code)
125+
return r.text
126+
127+
async def main():
128+
""" Main function."""
129+
asyncio.create_task(fetch("https://example.org"))
130+
print("Task launched and not awaited!")
131+
# Sleep to allow the fetch task to complete before the program exits.
132+
await asyncio.sleep(2)
133+
if __name__ == "__main__":
134+
asyncio.run(main())
135+
```
136+
137+
![asynchronous code result](images/02_httpx_async.png)
138+
139+
![asynchronous](images/asynchronous_simple.png)
140+
141+
The real payoff of async comes when you have **many requests to make**. With `asyncio.gather()`, you can fire all of them concurrently so the total wall-clock time is roughly that of the single slowest response — instead of the sum of all response times. That is exactly the pattern used in `example_async_gather.py` and `async_call_nb.ipynb` examples for fetching multiple RICs.
142+
143+
## Throttling and Rate Limits
144+
145+
The Data Platform API request limits (throttles) to effectively manage and protect its service and ensure fair usage across the non-streaming content.
146+
147+
An application would receive an error from the API call if an application reached or exceeds a limit (especially with the Asynchronous HTTP calls). You required to make some necessary adjustments to rectify the interaction with the API and retry the respective API call.
148+
149+
Two different server errors on API request limits are:
150+
151+
| **HTTP Status** | **Detail** |
152+
| --- | --- |
153+
| **429** | **Error Message**: too many attempts |
154+
| | **Description**: A per account limit where the number of requests per second is limited for each account accessing the platform. If this limit is reached, applications will receive a standard HTTP error (HTTP 429 too many requests). |
155+
| | **Suggestion**: Please reduce the number of requests per second and retry. |
156+
157+
Please find more detail regarding the Data Platform HTTP error status messages from the [RDP API General Guidelines](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis/documentation) document page.
158+
159+
The Historical Pricing endpoint rate limits information is available on the **Reference** tab of the [Data Platform API Playground](https://apidocs.refinitiv.com/Apps/ApiDocs) page. The current rate limits (**As of Mar 2026**) is as follows:
160+
161+
![historical rate limit](images/historical-pricing-ratelimits.png)
78162

79-
Demonstrates:
80-
- `POST /auth/oauth2/v1/token` — OAuth 2.0 Password Grant authentication
81-
- `GET /data/pricing/chains/v1/` — chain constituent lookup
82-
- `POST /data/historical-pricing/v1/views/events` — historical trade events for multiple RICs (commented out, ready to enable)
83-
- Refresh token flow (`grant_type=refresh_token`) — commented out, ready to enable
84-
- `POST /auth/oauth2/v1/revoke` — session revocation — commented out, ready to enable
85-
- Environment validation with a `_require_env()` helper that fails fast on missing credentials
86163

87164
## Security Notes
88165

89166
- All examples use `verify=False` to disable TLS certificate verification. This is intended for local/dev environments only (e.g. where a TLS-inspecting proxy such as ZScaler is in use). Remove `verify=False` or supply a proper CA bundle for production use.
90-
- Do not log or print access tokens in production applications.
167+
- Do not log or print access tokens in production applications.
168+
169+
## Code Walkthrough
170+
171+
Now we come to the code walkthrough. This article focuses primarily on the asynchronous code. Synchronous equivalents are shown in select places for comparison.
172+
173+
The examples use the following Python libraries for demonstration in Jupyter Notebook files.
174+
175+
| Library | Purpose |
176+
| --- | --- |
177+
| `asyncio` | Python's built-in async event loop and concurrency primitives |
178+
| `os` | Read environment variables |
179+
| `time` | Wall-clock timing via `time.perf_counter()` |
180+
| `httpx` | Async HTTP client |
181+
| `IPython.display` | Render formatted Markdown output in the notebook |
182+
| `dotenv` | Load credentials from `src/.env` |
183+
184+
### Data Platform Authentication
185+
186+
Let's start with the authentication. The first step of any application workflow is to log in to the RDP Auth Service.
187+
188+
The required credentials are:
189+
190+
- **Username**: The machine ID associated with your account.
191+
- **Password**: The password for the machine ID.
192+
- **Client ID (AppKey)**: A unique identifier for your app, generated via the App Key Generator. Keep it private.
193+
- **Grant Type `password`**: Used for the initial authentication request with a username/password combination.
194+
195+
I strongly suggest reading the [Data Platform: Authorization - All about tokens](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis/tutorials#authorization-all-about-tokens) tutorial for a deeper understanding of RDP authentication.
196+
197+
The authentication function uses Python's [`async`](https://docs.python.org/3/reference/compound_stmts.html#async-def)/[`await`](https://docs.python.org/3/reference/expressions.html#await) syntax so the HTTP request can be suspended and resumed when the network response arrives — without blocking other tasks. The `client` parameter is a shared `httpx.AsyncClient` instance passed in from the caller, so the same underlying TCP connection pool is reused across all requests rather than opening a new connection each time.
198+
199+
```python
200+
async def post_authentication_async(machine_id, password, app_key, url, client):
201+
"""Authenticate to RDP and return the token response as JSON."""
202+
203+
# Build the OAuth 2.0 Password Grant request payload.
204+
# Sent as application/x-www-form-urlencoded (httpx encodes a dict automatically).
205+
payload = {
206+
"username": machine_id,
207+
"password": password,
208+
"grant_type": "password",
209+
"scope": "trapi",
210+
"takeExclusiveSignOnControl": "true",
211+
"client_id": app_key
212+
}
213+
214+
# Send authentication request to the OAuth token endpoint.
215+
# `data=payload` sends a form body required by this endpoint.
216+
response_auth = await client.post(url, data=payload, headers=headers)
217+
# Raise for 4xx/5xx API failures.
218+
response_auth.raise_for_status()
219+
return response_auth.json()
220+
```
221+
222+
The `raise_for_status()` call handles any non-[HTTP 200 OK](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200) response — such as 4xx or 5xx errors — by raising an exception that propagates back to the caller.
223+
224+
Moving on to the main code. The `async with` block opens a shared `httpx.AsyncClient` and guarantees its connection pool is closed cleanly when the block exits, whether it completes normally or raises an exception. Inside the block, `post_authentication_async()` *is awaited* to obtain the Bearer token before any data requests are made.
225+
226+
```python
227+
# Main Code
228+
async with httpx.AsyncClient(
229+
verify=False,
230+
base_url=base_url,
231+
timeout=10.0,
232+
follow_redirects=True,
233+
) as client:
234+
# --- Authentication (must complete before any data requests) ---
235+
try:
236+
token_data = await post_authentication_async(machine_id, password, app_key, AUTH_TOKEN_URL, client)
237+
print("Authentication successful. Access token obtained.")
238+
239+
access_token = token_data.get("access_token")
240+
241+
# --- Exception handlers ordered from most-specific to least-specific ---
242+
except httpx.HTTPStatusError as e:
243+
# Server returned a 4xx or 5xx status code.
244+
print(f"HTTP error during request: {e.request.url} {e.response.status_code} - {e.response.text}")
245+
except httpx.TimeoutException as e:
246+
# Request exceeded the configured timeout (must precede RequestError
247+
# because TimeoutException is a subclass of RequestError).
248+
print(f"Timeout error: {e}")
249+
except httpx.RequestError as e:
250+
# Network-level failure: DNS, connection refused, SSL error, etc.
251+
print(f"Network error: {e}")
252+
except Exception as e:
253+
# Catch-all for unexpected errors (e.g. JSON decode, assertion).
254+
print(f"Unexpected error: {e}")
255+
```
256+
257+
### Where is asyncio.run(main())?
258+
259+
You might wonder why the main code does not call `asyncio.run(main())`. The reason is that Jupyter natively supports top-level `await`, so no `asyncio.run()` wrapper is needed.
260+
261+
### Comparing to Synchronous Code
262+
263+
For a single HTTP request, the synchronous equivalent is *almost* identical. The only real differences are the absence of `async`/`await` and the use of `httpx.Client` instead of `httpx.AsyncClient`. The code runs line by line — each statement blocks and waits for the network response before moving on.
264+
265+
```python
266+
def post_authentication(machine_id, password, app_key, url, client):
267+
"""Authenticate to RDP and return the token response as JSON."""
268+
269+
payload = { ... } # same as the Async Code
270+
# no await
271+
response = client.post(url, data=payload)
272+
response.raise_for_status()
273+
return response.json()
274+
275+
...
276+
277+
# Main code, use httpx.Client.
278+
with httpx.Client(
279+
verify=False,
280+
base_url=base_url,
281+
timeout=10.0,
282+
default_encoding="utf-8",
283+
follow_redirects=True,
284+
) as client:
285+
try:
286+
# Authenticate and get the access token.
287+
auth_response = post_authentication(machine_id, password, app_key, AUTH_TOKEN_URL, client)
288+
access_token = auth_response["access_token"]
289+
print("Authentication successful! Access token obtained.\n")
290+
except httpx.HTTPStatusError as exc:
291+
print(f"HTTP error occurred during HTTP Request: {exc.request.url}: {exc.response.status_code} - {exc.response.text}")
292+
...
293+
```
294+
295+
296+
297+

0 commit comments

Comments
 (0)