pyIPO
A production-grade Python library for fetching and parsing IPO data from the Nasdaq calendar API. Designed for serverless contexts (AWS Lambda) and programmatic pipelines — strictly typed, stateless, and zero-dependency beyond curl_cffi.
Table of Contents
Features
- Strict types — all deals returned as frozen
IPODealdataclass instances; no raw dicts leak through the public surface - 4 deal lifecycle states —
FILING,UPCOMING,PRICED,WITHDRAWN - Chrome TLS impersonation — uses
curl_cffito bypass Akamai/Cloudflare WAF filtering applied to standard HTTP clients - Serverless-safe — 10-second request timeout ceiling, stateless client, no global connections
- Rolling window queries — fetch across N months in one call, with automatic deduplication
- Resilient parsing — null-flag normalisation (
"N/A","TBD",""→None), price-range splitting, shorthand share counts ("1.5M"→1_500_000)
Installation
pip install -e .
Requires Python ≥ 3.10. The only runtime dependency is curl_cffi ≥ 0.6.
Quick Start
from pyipo import NasdaqClient, DealState
client = NasdaqClient()
# All deals across a rolling 3-month window (default)
deals = client.get_ipos()
# Filter by state
upcoming = [d for d in deals if d.deal_state == DealState.UPCOMING]
priced = [d for d in deals if d.deal_state == DealState.PRICED]
for deal in upcoming:
print(f"{deal.company_name} ({deal.symbol}) — {deal.price_low}–{deal.price_high} — {deal.expected_date}")
Single-month query:
deals = client.get_calendar("2024-06")
Public API
NasdaqClient
class NasdaqClient:
def get_ipos(self, lookback_months: int = 3) -> list[IPODeal]: ...
def get_calendar(self, month: str) -> list[IPODeal]: ...
get_ipos(lookback_months=3)
Queries the rolling window of lookback_months calendar months (current month + N−1 prior months). Months are fetched sequentially with a 1-second polite throttle between requests. Deals with the same (company_name, deal_state) appearing in multiple months are deduplicated — only the first occurrence is kept.
| Parameter | Type | Default | Description |
|---|---|---|---|
lookback_months |
int |
3 |
Number of months to look back from today |
Returns: list[IPODeal]
Raises: NasdaqAPIError on network or HTTP errors
get_calendar(month)
Fetches all IPO deals for a single specific month.
| Parameter | Type | Description |
|---|---|---|
month |
str |
Month in 'YYYY-MM' format, e.g. "2024-06" |
Returns: list[IPODeal]
Raises: NasdaqAPIError on network or HTTP errors
IPODeal
Immutable (frozen=True) dataclass representing a single IPO deal.
from pyipo import IPODeal
| Field | Type | States | Description |
|---|---|---|---|
company_name |
str |
all | Legal company name |
deal_state |
DealState |
all | Lifecycle stage |
symbol |
Optional[str] |
all | Proposed ticker symbol |
market |
Optional[str] |
UPCOMING, PRICED | Exchange (NASDAQ, NYSE, …) |
offer_amount |
Optional[float] |
all | Total dollar value of shares offered |
shares_outstanding |
Optional[int] |
UPCOMING, PRICED | Number of shares offered |
price_low |
Optional[float] |
UPCOMING | Lower bound of proposed price range |
price_high |
Optional[float] |
UPCOMING | Upper bound of proposed price range |
actual_price |
Optional[float] |
PRICED | Final offer price |
expected_date |
Optional[date] |
UPCOMING | Expected pricing date |
filing_date |
Optional[date] |
FILING, PRICED | Date of S-1 filing or pricing |
withdrawn_date |
Optional[date] |
WITHDRAWN | Date the deal was pulled |
None values mean the data was absent or a null-flag ("N/A", "TBD", etc.) in the source.
DealState
from pyipo import DealState
DealState.FILING # "filing" — S-1 filed, no firm date yet
DealState.UPCOMING # "upcoming" — On the calendar, roadshow active
DealState.PRICED # "priced" — Final price set, ready to trade
DealState.WITHDRAWN # "withdrawn"— Deal pulled / cancelled
NasdaqAPIError
from pyipo import NasdaqAPIError
try:
deals = client.get_ipos()
except NasdaqAPIError as e:
print(e) # human-readable message
print(e.status_code) # int HTTP status, or None for connection errors
Raised on:
- HTTP responses with status ≠ 200
- Connection timeouts (hard ceiling: 10 seconds)
- Any network-layer failure (DNS, TLS, reset)
Data Model
All four deal states are sourced from a single Nasdaq endpoint:
GET https://api.nasdaq.com/api/ipo/calendar?date=YYYY-MM
The response contains four top-level buckets which map directly to DealState:
| Bucket | Response key | DealState |
|---|---|---|
| Early filings | data.filed.rows |
FILING |
| Active roadshows | data.upcoming.upcomingTable.rows |
UPCOMING |
| Priced deals | data.priced.rows |
PRICED |
| Cancelled deals | data.withdrawn.rows |
WITHDRAWN |
Error Handling
from pyipo import NasdaqClient, NasdaqAPIError
client = NasdaqClient()
try:
deals = client.get_calendar("2024-06")
except NasdaqAPIError as e:
if e.status_code == 429:
# Rate-limited — back off and retry
...
elif e.status_code is None:
# Connection failure (timeout, DNS, TLS)
...
else:
# Unexpected HTTP error
raise
Development
# Clone and install with dev dependencies
pip install -e ".[dev]"
# Run tests (no network calls)
pytest tests/ -v -p no:typeguard
# Run a single test file
pytest tests/test_parsers.py -v -p no:typeguard
Note: The
-p no:typeguardflag works around a brokentypeguardpytest plugin that may be present in some environments. It has no effect iftypeguardis not installed.
Project Structure
pyIPO/
├── src/
│ └── pyipo/
│ ├── __init__.py # Public exports
│ ├── enums.py # DealState enum
│ ├── models.py # IPODeal dataclass
│ ├── client.py # HTTP transport (curl_cffi)
│ ├── parsers.py # JSON → IPODeal conversion
│ └── exceptions.py # NasdaqAPIError
├── tests/
│ ├── conftest.py # Frozen JSON fixtures
│ ├── test_parsers.py # Parser unit tests (36 tests)
│ └── test_client.py # Client integration tests (15 tests)
├── pyproject.toml
└── ipo.py # Original standalone script (kept for reference)
Architecture
NasdaqClient
│
├── get_ipos(lookback_months) ← rolling window, dedup
└── get_calendar(month) ← single month
│
└── _fetch_month(month) ← curl_cffi GET, raises NasdaqAPIError
│
└── _parse_all(data)
├── parse_filings(rows) → list[IPODeal(FILING)]
├── parse_upcoming(rows) → list[IPODeal(UPCOMING)]
├── parse_priced(rows) → list[IPODeal(PRICED)]
└── parse_withdrawn(rows) → list[IPODeal(WITHDRAWN)]
Parsing helpers in parsers.py handle all edge cases internally — callers always receive clean, typed values or None; raw strings never escape the parser layer.
Release files for pyipo 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyipo-0.2.1.tar.gz | 13.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyipo-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.3 kB
Release files / pyipo-0.2.1.tar.gz
| Download URL | pyipo-0.2.1.tar.gz |
|---|---|
| Size | 13.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
77089f2fdb2553aed30a0878f703ab19138cd5111bf61e3d35e99bec89577f0b
|
|
BLAKE2b-256 checksum How to use checksums |
84a4e9f6e2149d3b1c18fc1b6d355ca042287e727a6463067a66cc4ae89a9016
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.8
|
Release files / pyipo-0.2.1-py3-none-any.whl
| Download URL | pyipo-0.2.1-py3-none-any.whl |
|---|---|
| Size | 18.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a1ed6e75fcad9dc457008c4213a94a6ed57542f2c0f0cbec7a947b8b7c914216
|
|
BLAKE2b-256 checksum How to use checksums |
88a0b43e0607ab9f30e6a508bd5c4be57a31a494aee74d1135457d1c7f8b17f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.8
|