Source code for openbasket.sources.sec

"""Downloads from sec.gov.

The SEC asks automated clients to send a User-Agent naming the requester and
an email address, and to stay under ten requests a second. Set
``SEC_USER_AGENT`` in ``.env``.
"""

from __future__ import annotations

import logging
import os
import time
from pathlib import Path

import pandas as pd
import requests

logger = logging.getLogger(__name__)

NPORT_URL = "https://www.sec.gov/files/dera/data/form-n-port-data-sets/{quarter}_nport.zip"
FUND_TICKERS_URL = "https://www.sec.gov/files/company_tickers_mf.json"


def user_agent() -> str:
    ua = os.environ.get("SEC_USER_AGENT", "").strip()
    if not ua:
        raise RuntimeError("Set SEC_USER_AGENT in .env, e.g. 'openbasket research you@example.org'.")
    return ua


def _session() -> requests.Session:
    s = requests.Session()
    s.headers.update({"User-Agent": user_agent(), "Accept-Encoding": "gzip, deflate"})
    return s


[docs] def download(url: str, dest: Path, overwrite: bool = False, timeout: int = 1800) -> Path: """Stream ``url`` to ``dest`` unless it is already there.""" dest = Path(dest) if dest.exists() and not overwrite: logger.info("Using cached %s", dest) return dest dest.parent.mkdir(parents=True, exist_ok=True) tmp = dest.with_suffix(dest.suffix + ".part") logger.info("Downloading %s", url) with _session().get(url, stream=True, timeout=timeout) as r: r.raise_for_status() with open(tmp, "wb") as fh: for chunk in r.iter_content(chunk_size=1 << 20): fh.write(chunk) tmp.replace(dest) time.sleep(0.2) return dest
[docs] def fund_tickers() -> pd.DataFrame: """SEC's list of fund share classes with tickers. Columns: ``cik``, ``series_id``, ``class_id``, ``ticker``. It includes ETFs; unit investment trusts such as SPY are absent because they do not file N-PORT. """ r = _session().get(FUND_TICKERS_URL, timeout=60) r.raise_for_status() doc = r.json() df = pd.DataFrame(doc["data"], columns=doc["fields"]) df = df.rename(columns={"seriesId": "series_id", "classId": "class_id", "symbol": "ticker"}) df["cik"] = df["cik"].astype(str).str.zfill(10) df["ticker"] = df["ticker"].str.upper().str.strip() return df[["cik", "series_id", "class_id", "ticker"]]