Metadata-Version: 2.4
Name: pyipo
Version: 0.1.1
Summary: A production-grade programmatic wrapper for the Nasdaq IPO calendar API with TLS impersonation.
Project-URL: Homepage, https://github.com/yourusername/pyIPO
Project-URL: Documentation, https://github.com/yourusername/pyIPO#readme
Project-URL: Issues, https://github.com/yourusername/pyIPO/issues
Author-email: Your Name <your.email@example.com>
License: MIT
Keywords: calendar,curl-cffi,finance,ipo,nasdaq,serverless
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.10
Requires-Dist: curl-cffi>=0.6.0
Description-Content-Type: text/markdown

# 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

- [pyIPO](#pyipo)
  - [Table of Contents](#table-of-contents)
  - [Features](#features)
  - [Installation](#installation)
  - [Quick Start](#quick-start)
  - [Public API](#public-api)
    - [NasdaqClient](#nasdaqclient)
      - [`get_ipos(lookback_months=3)`](#get_iposlookback_months3)
      - [`get_calendar(month)`](#get_calendarmonth)
    - [IPODeal](#ipodeal)
    - [DealState](#dealstate)
    - [NasdaqAPIError](#nasdaqapierror)
  - [Data Model](#data-model)
  - [Error Handling](#error-handling)
  - [Development](#development)
    - [Project Structure](#project-structure)
  - [Architecture](#architecture)

---

## Features

- **Strict types** — all deals returned as frozen `IPODeal` dataclass instances; no raw dicts leak through the public surface
- **4 deal lifecycle states** — `FILING`, `UPCOMING`, `PRICED`, `WITHDRAWN`
- **Chrome TLS impersonation** — uses `curl_cffi` to 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

```bash
pip install -e .
```

Requires Python ≥ 3.10. The only runtime dependency is [`curl_cffi`](https://github.com/yifeikong/curl_cffi) ≥ 0.6.

---

## Quick Start

```python
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:

```python
deals = client.get_calendar("2024-06")
```

---

## Public API

### NasdaqClient

```python
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.

```python
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

```python
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

```python
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

```python
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

```bash
# 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:typeguard` flag works around a broken `typeguard` pytest plugin that may be present in some environments. It has no effect if `typeguard` is 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.

