Skip to content

Commit 810f106

Browse files
committed
Updated README.md
Added example_async_gather.py with Asyncio Gather method Added example_async_simple.py with simple async method
1 parent f21c05e commit 810f106

7 files changed

Lines changed: 452 additions & 264 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,5 @@ pyrightconfig.json
177177

178178
# Virtual environment
179179
.venv/
180+
181+
src/example_async_taskgroup.py

README.md

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,79 @@
1-
# RDP_HTTPX
1+
# Data Platform APIs HTTP REST Application using HTTPX
22

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.
3+
- Version: 1.0
4+
- Last update: Mar 2026
5+
- Environment: Python + JupyterLab + Data Platform Account
6+
- Prerequisite: Data Platform access/entitlements
7+
8+
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 OAuth 2.0 Password Grant, then call sample REST endpoints — covering both synchronous and asynchronous patterns.
9+
10+
## Project Structure
11+
12+
```
13+
├── requirements.txt # Pinned dependencies
14+
src/
15+
├── .env.example # Environment variable template
16+
├── example_sync_httpx.py # Synchronous — direct httpx module calls (no shared client)
17+
├── example_client.py # Synchronous — shared httpx.Client
18+
├── example_async_simple.py # Async — sequential awaits in a loop
19+
├── example_async_gather.py # Async — asyncio.gather() with Semaphore
20+
└── async_learn.py # Learning script — ExceptionGroup / except*
21+
```
422

523
## Included Scripts
624

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`
25+
### `src/example_sync_httpx.py` — Synchronous, direct `httpx` calls
26+
27+
Simplest synchronous example. Each function calls `httpx.get()` / `httpx.post()` directly — no shared client or connection pool. Good as a minimal reference or quick script.
28+
29+
Demonstrates:
30+
- `POST /auth/oauth2/v1/token` — OAuth 2.0 Password Grant authentication
31+
- `GET /data/pricing/chains/v1/` — chain constituent lookup for a single RIC
32+
- `POST /data/historical-pricing/v1/views/events` — historical trade events for multiple RICs
33+
- Refresh token flow (`grant_type=refresh_token`)
34+
- `POST /auth/oauth2/v1/revoke` — session revocation using HTTP Basic Auth
35+
- Per-call `verify=False` passed directly to each `httpx` function
36+
37+
### `src/example_client.py` — Synchronous with shared client
38+
39+
Synchronous (blocking) script using a single shared `httpx.Client` instance for connection pooling and consistent configuration across all requests.
40+
41+
Demonstrates:
42+
- `POST /auth/oauth2/v1/token` — OAuth 2.0 Password Grant authentication
43+
- `GET /data/pricing/chains/v1/` — chain constituent lookup
44+
- `POST /data/historical-pricing/v1/views/events` — historical trade events for multiple RICs (commented out, ready to enable)
45+
- Refresh token flow (`grant_type=refresh_token`) — commented out, ready to enable
46+
- `POST /auth/oauth2/v1/revoke` — session revocation — commented out, ready to enable
47+
- Environment validation with a `_require_env()` helper that fails fast on missing credentials
48+
49+
### `src/example_async_simple.py` — Async, sequential loop
50+
51+
Async script using `httpx.AsyncClient`. Fetches interday-summaries for each RIC one after another inside a `for` loop. Simple starting point before introducing concurrency.
52+
53+
Demonstrates:
54+
- `POST /auth/oauth2/v1/token` — async authentication
55+
- `GET /data/historical-pricing/v1/views/interday-summaries/{ric}` — daily OHLCV data with corporate-action adjustments
56+
- Sequential `await` per RIC — no concurrent requests
1557

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.
58+
### `src/example_async_gather.py` — Async with `asyncio.gather()` and `Semaphore`
59+
60+
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.
61+
62+
Demonstrates:
63+
- `POST /auth/oauth2/v1/token` — async authentication
64+
- `GET /data/historical-pricing/v1/views/interday-summaries/{ric}` — concurrent fetches for 10 RICs
65+
- `asyncio.Semaphore` — limits concurrent requests (default: 3)
66+
- `return_exceptions=True` — prevents one failure from cancelling the rest; each result is inspected individually
67+
- Per-result error handling: `httpx.HTTPStatusError`, `httpx.RequestError`, generic `Exception`
68+
69+
70+
### `src/async_learn.py``ExceptionGroup` and `except*` sandbox
71+
72+
Standalone learning script demonstrating how `asyncio.gather(return_exceptions=True)` results can be re-raised as an `ExceptionGroup`, and how `except*` dispatches individual exception types from the group.
2073

2174
## Prerequisites
2275

23-
- Python 3.10+
76+
- Python 3.11+ (required for `asyncio.TaskGroup` and `except*`)
2477
- LSEG RDP credentials:
2578
- Machine ID
2679
- Password
@@ -43,9 +96,7 @@ python -m venv .venv
4396
pip install -r requirements.txt
4497
```
4598

46-
3. Create your environment file.
47-
48-
Copy `src/.env.example` to `src/.env` and fill in values:
99+
3. Create your environment file by creating `src/.env` with the following content:
49100

50101
```dotenv
51102
RDP_BASE_URL=https://api.refinitiv.com
@@ -58,27 +109,33 @@ APPKEY_RDP=<RDP AppKey>
58109
## Run
59110

60111
```powershell
61-
python .\src\example_sync_httpx.py
62-
```
63-
64-
```powershell
112+
# Synchronous
65113
python .\src\example_client.py
114+
115+
# Async — sequential loop
116+
python .\src\example_async_simple.py
117+
118+
# Async — concurrent via asyncio.gather()
119+
python .\src\example_async_gather.py
120+
121+
# Async — concurrent via asyncio.TaskGroup
122+
python .\src\example_async_taskgroup.py
66123
```
67124

68-
For demo purposes, scripts print token/output payloads and endpoint responses.
125+
Each script prints the authenticated request URLs and JSON responses. Timing is printed on exit for the async scripts.
69126

70127
## Security Notes
71128

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.
129+
- 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.
130+
- Do not log or print access tokens in production applications.
75131

76132
## License
77133

78134
Apache 2.0. See [LICENSE.md](LICENSE.md).
79135

80-
## Reference
136+
## References
81137

82138
- https://realpython.com/async-io-python/
83139
- https://www.twilio.com/en-us/blog/asynchronous-http-requests-in-python-with-httpx-and-asyncio
140+
- https://docs.python.org/3/library/asyncio-task.html#task-groups
84141

src/async_learn.py

Lines changed: 22 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,30 @@
11
import asyncio
22
import time
33

4+
async def coro_a():
5+
await asyncio.sleep(1)
6+
raise ValueError("Error in coro A")
47

5-
async def task_function(name, duration, fail=False):
6-
print(f" Task {name}: Starting (will sleep for {duration}s)")
7-
await asyncio.sleep(duration)
8-
if fail:
9-
raise ValueError(f"Task {name} failed!")
10-
print(f" Task {name}: Finished")
11-
return f"Result of {name}"
8+
async def coro_b():
9+
await asyncio.sleep(1)
10+
raise TypeError("Error in coro B")
1211

13-
14-
# ─────────────────────────────────────────────
15-
# asyncio.gather
16-
# ─────────────────────────────────────────────
17-
# • Available since Python 3.4
18-
# • Accepts coroutines or Tasks
19-
# • Returns a list of results in the same order as inputs
20-
# • Error behaviour (default): if one task raises, the exception is
21-
# propagated immediately to the caller, but the OTHER tasks keep
22-
# running in the background (they are NOT cancelled).
23-
# • return_exceptions=True: exceptions are returned as result values
24-
# instead of being raised, so all tasks always run to completion.
25-
# • Good for fire-and-forget fan-out or when you need return_exceptions.
26-
27-
async def demo_gather():
28-
print("\n=== asyncio.gather (no failures) ===")
29-
start = time.perf_counter()
30-
results = await asyncio.gather(
31-
task_function("A", 3),
32-
task_function("B", 1),
33-
task_function("C", 2),
34-
)
35-
print(f" Done in {time.perf_counter() - start:.2f}s | Results: {results}")
36-
37-
print("\n=== asyncio.gather with return_exceptions=True ===")
38-
start = time.perf_counter()
39-
results = await asyncio.gather(
40-
task_function("A", 1),
41-
task_function("B", 0.5, fail=True), # will raise
42-
task_function("C", 1),
43-
return_exceptions=True, # exceptions become values
44-
)
45-
print(f" Done in {time.perf_counter() - start:.2f}s | Results: {results}")
46-
47-
48-
# ─────────────────────────────────────────────
49-
# asyncio.TaskGroup (Python 3.11+)
50-
# ─────────────────────────────────────────────
51-
# • Context-manager-based structured concurrency.
52-
# • Tasks are created with tg.create_task() inside the `async with` block.
53-
# • When the block exits all tasks are AWAITED automatically.
54-
# • Error behaviour: if ANY task raises, ALL remaining tasks are
55-
# cancelled immediately, then the exception is re-raised.
56-
# Multiple simultaneous exceptions are collected into an ExceptionGroup.
57-
# • Cleaner resource management; preferred for structured concurrency.
58-
59-
async def demo_task_group():
60-
print("\n=== asyncio.TaskGroup (no failures) ===")
61-
start = time.perf_counter()
62-
results = []
63-
async with asyncio.TaskGroup() as tg:
64-
t1 = tg.create_task(task_function("A", 3))
65-
t2 = tg.create_task(task_function("B", 1))
66-
t3 = tg.create_task(task_function("C", 2))
67-
# All tasks are guaranteed finished here
68-
results = [t1.result(), t2.result(), t3.result()]
69-
print(f" Done in {time.perf_counter() - start:.2f}s | Results: {results}")
70-
71-
print("\n=== asyncio.TaskGroup with a failing task ===")
72-
try:
73-
async with asyncio.TaskGroup() as tg:
74-
tg.create_task(task_function("A", 1))
75-
tg.create_task(task_function("B", 0.1, fail=True)) # raises quickly
76-
tg.create_task(task_function("C", 1)) # will be cancelled
77-
except* ValueError as eg:
78-
print(f" Caught ExceptionGroup errors: {eg}")
79-
80-
81-
# ─────────────────────────────────────────────
82-
# Summary
83-
# ─────────────────────────────────────────────
84-
# | Feature | gather | TaskGroup (3.11+) |
85-
# |--------------------------|-------------------------|-------------------------|
86-
# | API style | function call | async context manager |
87-
# | Task creation | pass coroutines/tasks | tg.create_task() |
88-
# | On first exception | others keep running * | others are cancelled |
89-
# | Multiple exceptions | only first propagated | ExceptionGroup |
90-
# | return_exceptions | yes | no (use try/except) |
91-
# | Structured concurrency | no | yes |
92-
# | Python version | 3.4+ | 3.11+ |
93-
#
94-
# * Unless return_exceptions=False (default) and you shield tasks.
12+
async def coro_c():
13+
await asyncio.sleep(1)
14+
raise IndexError("Error in coro C")
9515

9616
async def main():
97-
await demo_gather()
98-
await demo_task_group()
99-
100-
17+
results = await asyncio.gather(coro_a(), coro_b(), coro_c(), return_exceptions=True)
18+
exceptions = [e for e in results if isinstance(e, Exception)]
19+
if exceptions:
20+
raise ExceptionGroup("Multiple exceptions occurred", exceptions)
21+
10122
if __name__ == "__main__":
102-
asyncio.run(main())
103-
104-
23+
try:
24+
asyncio.run(main())
25+
except* ValueError as ve_group:
26+
print(f"[ValueError handled] {ve_group.exceptions}")
27+
except* TypeError as te_group:
28+
print(f"[TypeError handled] {te_group.exceptions}")
29+
except* IndexError as ie_group:
30+
print(f"[IndexError handled] {ie_group.exceptions}")

0 commit comments

Comments
 (0)