Skip to content

Commit 0c31eda

Browse files
committed
Added HTTPX client example
Updated README
1 parent e9c05dd commit 0c31eda

2 files changed

Lines changed: 217 additions & 22 deletions

File tree

README.md

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,49 @@
11
# RDP_HTTPX
22

3-
Small Python example that uses [`httpx`](https://www.python-httpx.org/) to authenticate with the [Data Platform APIs](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis) (RDP, also known as Delivery Platform) OAuth2 **Password Grant** flow and call a couple of RDP REST endpoints.
3+
Small Python examples that use [`httpx`](https://www.python-httpx.org/) to authenticate with [LSEG Data Platform APIs](https://developers.lseg.com/en/api-catalog/refinitiv-data-platform/refinitiv-data-platform-apis) (RDP, also known as Delivery Platform) using OAuth2 Password Grant, then call sample REST endpoints.
44

5-
## What’s included
5+
## Included Scripts
66

7-
- `src/example_sync_httpx.py` – synchronous (blocking) demo script that:
8-
- Authenticates: `POST /auth/oauth2/v1/token`
9-
- Calls Pricing Chains: `GET /data/pricing/chains/v1/`
10-
- Calls Historical Pricing Events: `POST /data/historical-pricing/v1/views/events`
11-
- Refreshes token (refresh_token grant)
12-
- Revokes token: `POST /auth/oauth2/v1/revoke`
7+
- `src/example_sync_httpx.py`
8+
- Synchronous (blocking) requests with direct `httpx` calls.
9+
- Demonstrates:
10+
- `POST /auth/oauth2/v1/token`
11+
- `GET /data/pricing/chains/v1/`
12+
- `POST /data/historical-pricing/v1/views/events`
13+
- Refresh token flow (`grant_type=refresh_token`)
14+
- `POST /auth/oauth2/v1/revoke`
15+
16+
- `src/example_client.py`
17+
- Synchronous (blocking) script using one shared `httpx.Client` (connection pooling + shared config).
18+
- Includes simple environment validation and reusable helper methods.
19+
- Demonstrates the same endpoint flow as above.
1320

1421
## Prerequisites
1522

16-
- Python 3.10+ recommended
17-
- RDP credentials:
18-
- **Machine ID**
19-
- **Password**
20-
- **AppKey**
23+
- Python 3.10+
24+
- LSEG RDP credentials:
25+
- Machine ID
26+
- Password
27+
- AppKey
2128

22-
Please contact LSEG Representative and Account manager to help you get Data Platform access.
29+
If you do not have access yet, contact your LSEG representative or account manager.
2330

2431
## Setup
2532

26-
1) Create and activate a virtual environment.
33+
1. Create and activate a virtual environment.
2734

2835
```powershell
2936
python -m venv .venv
3037
.\.venv\Scripts\Activate.ps1
3138
```
3239

33-
2) Install dependencies.
40+
2. Install dependencies.
3441

3542
```powershell
3643
pip install -r requirements.txt
3744
```
3845

39-
3) Create your environment file.
46+
3. Create your environment file.
4047

4148
Copy `src/.env.example` to `src/.env` and fill in values:
4249

@@ -54,14 +61,18 @@ APPKEY_RDP=<RDP AppKey>
5461
python .\src\example_sync_httpx.py
5562
```
5663

57-
The script prints the access token (demo only), then prints the JSON responses from the sample endpoints.
64+
```powershell
65+
python .\src\example_client.py
66+
```
5867

59-
## Security notes
68+
For demo purposes, scripts print token/output payloads and endpoint responses.
6069

61-
The example uses `verify=False` in `httpx` calls, which **disables TLS certificate verification**. This is unsafe for production—remove `verify=False` (or provide a proper CA bundle) for real usage. I use it in this project to avoid LSEG beloved ZScaler.
70+
## Security Notes
6271

63-
Also avoid printing access tokens in real applications.
72+
- The examples currently use `verify=False`, which disables TLS certificate verification.
73+
- This is not safe for production. Remove `verify=False` (or provide a proper CA bundle) for real usage.
74+
- Avoid printing access tokens in production applications/logs.
6475

6576
## License
6677

67-
Apache 2.0 — see [LICENSE.md](LICENSE.md).
78+
Apache 2.0. See [LICENSE.md](LICENSE.md).

src/example_client.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import json
2+
import os
3+
import time
4+
from typing import Any, Sequence
5+
6+
import httpx
7+
from dotenv import load_dotenv
8+
9+
10+
AUTH_TOKEN_URL = "/auth/oauth2/v1/token"
11+
AUTH_REVOKE_URL = "/auth/oauth2/v1/revoke"
12+
CHAIN_URL = "/data/pricing/chains/v1/"
13+
HISTORICAL_EVENT_URL = "/data/historical-pricing/v1/views/events"
14+
15+
16+
def _bearer_headers(token) -> dict[str, str]:
17+
"""Build common JSON headers with bearer auth."""
18+
return {
19+
"Authorization": f"Bearer {token}",
20+
"Content-Type": "application/json",
21+
}
22+
23+
24+
def _require_env(name) -> str:
25+
"""Read a required environment variable or fail early with a clear message."""
26+
value = os.getenv(name)
27+
if not value:
28+
raise ValueError(f"Missing required environment variable: {name}")
29+
return value
30+
31+
32+
def post_authentication(machine_id, password, app_key, url, client):
33+
"""Authenticate to RDP and return the token response as JSON."""
34+
35+
# Build the OAuth 2.0 Password Grant request payload.
36+
# Sent as application/x-www-form-urlencoded (httpx encodes a dict automatically).
37+
payload = {
38+
"username": machine_id, # RDP Machine-ID
39+
"password": password, # RDP Password
40+
"grant_type": "password", # OAuth 2.0 grant type
41+
"scope": "trapi", # Target API scope
42+
"takeExclusiveSignOnControl": "true", # Revoke other active sessions
43+
"client_id": app_key # RDP AppKey (acts as client_id)
44+
}
45+
46+
# Send authentication request to the OAuth token endpoint.
47+
# `data=payload` sends a form body required by this endpoint.
48+
# `verify=False` skips SSL verification (for local/dev only). This is not recommended for production use. I use it to avoid LSEG beloved ZScaler.
49+
response = client.post(url, data=payload)
50+
response.raise_for_status() # Raise for 4xx/5xx API failures.
51+
return response.json()
52+
53+
54+
def get_chain(ric, token, url, client):
55+
"""Fetch chain data for a single RIC symbol using an access token."""
56+
headers = _bearer_headers(token)
57+
# Query string parameters sent with the GET request.
58+
parameters = {
59+
"universe": ric
60+
}
61+
# Request chain data from the pricing chains endpoint.
62+
# `verify=False` skips SSL verification (for local/dev only). This is not recommended for production use. I use it to avoid LSEG beloved ZScaler.
63+
response = client.get(url, params=parameters, headers=headers)
64+
response.raise_for_status()
65+
return response.json()
66+
67+
68+
def post_historical_event(rics, token, url, client):
69+
"""Request historical event data for multiple RICs."""
70+
headers = _bearer_headers(token)
71+
# JSON body for the historical pricing events request.
72+
payload = {
73+
"universe": rics,
74+
"eventTypes": ["trade"]
75+
}
76+
77+
# `json=payload` serializes and sends JSON in the request body.
78+
# `verify=False` skips SSL verification (for local/dev only). This is not recommended for production use. I use it to avoid LSEG beloved ZScaler.
79+
response = client.post(url, json=payload, headers=headers)
80+
response.raise_for_status()
81+
return response.json()
82+
83+
84+
def post_auth_refresh(app_key, refresh_token, url, client):
85+
"""Refresh the access token using the refresh token."""
86+
payload = {
87+
"grant_type": "refresh_token",
88+
"refresh_token": refresh_token,
89+
"client_id": app_key,
90+
}
91+
headers = {
92+
"Content-Type": "application/x-www-form-urlencoded"
93+
}
94+
# `verify=False` skips SSL verification (for local/dev only). This is not recommended for production use. I use it to avoid LSEG beloved ZScaler.
95+
response = client.post(url, data=payload, headers=headers)
96+
response.raise_for_status()
97+
return response.json()
98+
99+
100+
def post_auth_revoke(token, app_key, url, client):
101+
"""Revoke the access token to end the session."""
102+
headers = {
103+
"Content-Type": "application/x-www-form-urlencoded"
104+
}
105+
106+
payload = f"token={token}"
107+
auth = httpx.BasicAuth(username=app_key, password="")
108+
# `verify=False` skips SSL verification (for local/dev only). This is not recommended for production use. I use it to avoid LSEG beloved ZScaler.
109+
response = client.post(url, data=payload, headers=headers, auth=auth)
110+
response.raise_for_status()
111+
112+
113+
def main() -> None:
114+
"""Run the end-to-end demo: auth, chain data, and historical events."""
115+
# Load key/value pairs from src/.env into process environment.
116+
load_dotenv()
117+
118+
# Read credentials and base URL from environment variables.
119+
machine_id = _require_env("MACHINEID_RDP")
120+
password = _require_env("PASSWORD_RDP")
121+
app_key = _require_env("APPKEY_RDP")
122+
base_url = _require_env("RDP_BASE_URL")
123+
124+
# Reuse one connection pool across all requests.
125+
client = httpx.Client(
126+
verify=False,
127+
base_url=base_url,
128+
timeout=10.0,
129+
default_encoding="utf-8",
130+
follow_redirects=True,
131+
)
132+
133+
try:
134+
token_data = post_authentication(machine_id, password, app_key, AUTH_TOKEN_URL, client)
135+
print("Authentication successful. Access token obtained.")
136+
access_token = token_data.get("access_token")
137+
if access_token:
138+
print("Access Token:", json.dumps(access_token, indent=2))
139+
140+
ric = "EFX="
141+
print(f"Fetching chain data... for RIC: {ric}")
142+
chain_data = get_chain(ric, access_token, CHAIN_URL, client)
143+
print("Chain data retrieved successfully!")
144+
print(f"Chain data for {ric}:", json.dumps(chain_data, indent=2))
145+
146+
# Example multi-RIC request for historical events endpoint.
147+
rics = ["LSEG.L", "VOD.L", "BP.L"]
148+
print(f"Posting historical event data... for RICs: {rics}")
149+
historical_event_data = post_historical_event(rics, access_token, HISTORICAL_EVENT_URL, client)
150+
print("Historical event data retrieved successfully!")
151+
print(f"Historical event data for {rics}:", json.dumps(historical_event_data))
152+
153+
refresh_token = token_data.get("refresh_token")
154+
if refresh_token:
155+
time.sleep(5) # Sleep for 5 seconds before refreshing token (for demo purposes)
156+
print("Refreshing access token...")
157+
refreshed_token_data = post_auth_refresh(app_key, refresh_token, AUTH_TOKEN_URL, client)
158+
print("Token refreshed successfully!")
159+
print("New Access Token:", json.dumps(refreshed_token_data["access_token"], indent=2))
160+
else:
161+
print("No refresh token available. Cannot refresh access token.")
162+
163+
time.sleep(5) # Sleep for 5 seconds before revoking token (for demo purposes)
164+
print("Revoking access token...")
165+
post_auth_revoke(access_token, app_key, AUTH_REVOKE_URL, client)
166+
print("Access token revoked successfully.")
167+
168+
else:
169+
print("Failed to receive access token. Exiting...")
170+
return
171+
except httpx.HTTPStatusError as exc:
172+
print(f"HTTP error occurred during HTTP Request: {exc.request.url}: {exc.response.status_code} - {exc.response.text}")
173+
except httpx.RequestError as exc:
174+
print(f"An error occurred during HTTP Request: {exc.request.url}: {exc}")
175+
except ValueError as exc:
176+
print(f"Configuration error: {exc}")
177+
except KeyError as exc:
178+
print(f"An error occurred during HTTP Request: {exc}")
179+
finally:
180+
client.close()
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)