057bc4493e
Wraps the entire yfinance public API behind a single binary `yfin` with
51 subcommands across 13 namespaces (ticker, download, tickers, search,
lookup, market, sector, industry, calendars, screen, live, auth, config).
Output renders as Rich tables on TTY, JSON when piped; --format / --out
support table | json | ndjson | csv | tsv | parquet | yaml.
Highlights:
- 1857 LOC across 18 Python files under cli/yfin/
- Typer + Rich, packaged via hatchling + uv
- Install globally: `cd cli && uv tool install -e .`
- ~/.yfin/config.toml for persistent proxy/cookies/locale defaults
- Friendly YFRateLimitError handler (exit 2, no Rich traceback)
- Validated end-to-end with 33 live tests against Yahoo from a
residential exit (AAPL, MSFT, NVDA, options, screens, calendars,
financials, news, holders, parquet round-trip)
yfinance-tools.yaml is the single source of truth:
- tools (49) — every public yfinance entry point with parameters
- cli (13 groups) — exact CLI invocations, mapped back to tool names
- outputSchemas (27) — actual return shapes captured from live calls
- learnings (19) — IP rate-limit, crumb handshake, Typer gotchas,
screen-build wrapping rules, upstream stubs, etc.
CLAUDE.md documents the upstream codebase layout for future agents.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
6.5 KiB
Python
167 lines
6.5 KiB
Python
"""yfin root Typer app."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import typer
|
|
import yfinance as yf
|
|
from rich.console import Console
|
|
from yfinance.exceptions import YFRateLimitError
|
|
|
|
from . import __version__, config as cfg_mod, renderers
|
|
from ._shared import GlobalState
|
|
from .commands import (
|
|
auth as auth_cmd,
|
|
calendars as calendars_cmd,
|
|
config as config_cmd,
|
|
download as download_cmd,
|
|
industry as industry_cmd,
|
|
live as live_cmd,
|
|
lookup as lookup_cmd,
|
|
market as market_cmd,
|
|
screen as screen_cmd,
|
|
search as search_cmd,
|
|
sector as sector_cmd,
|
|
ticker as ticker_cmd,
|
|
tickers as tickers_cmd,
|
|
)
|
|
|
|
|
|
app = typer.Typer(
|
|
name="yfin",
|
|
no_args_is_help=True,
|
|
add_completion=True,
|
|
rich_markup_mode="rich",
|
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
help=(
|
|
"Single-binary CLI for the [bold]yfinance[/bold] library.\n\n"
|
|
"All 49 wrapped yfinance entry points are reachable as "
|
|
"[cyan]yfin <group> <subcommand>[/cyan]. Output defaults to a Rich table on "
|
|
"a TTY, JSON when piped. Override with [cyan]--format[/cyan] / [cyan]--out[/cyan]."
|
|
),
|
|
)
|
|
|
|
# Subcommand groups (must match cli.commandGroups in yfinance-tools.yaml).
|
|
app.add_typer(ticker_cmd.app, name="ticker", help="Per-symbol queries (one symbol per call).")
|
|
app.add_typer(tickers_cmd.app, name="tickers", help="Tickers-collection operations (history/news).")
|
|
app.add_typer(search_cmd.app, name="search", help="Yahoo Finance search by keyword.")
|
|
app.add_typer(lookup_cmd.app, name="lookup", help="Fuzzy symbol lookup.")
|
|
app.add_typer(market_cmd.app, name="market", help="Region-level overview / status.")
|
|
app.add_typer(sector_cmd.app, name="sector", help="Sector aggregates.")
|
|
app.add_typer(industry_cmd.app, name="industry", help="Industry aggregates.")
|
|
app.add_typer(calendars_cmd.app, name="calendars", help="Market-wide event calendars.")
|
|
app.add_typer(screen_cmd.app, name="screen", help="Predefined + custom market screens.")
|
|
app.add_typer(live_cmd.app, name="live", help="Stream live ticks via Yahoo WebSocket.")
|
|
app.add_typer(auth_cmd.app, name="auth", help="Yahoo cookie / strategy management.")
|
|
app.add_typer(config_cmd.app, name="config", help="yfin / yfinance config.")
|
|
|
|
# Top-level shortcut: `yfin download ...` (most-used multi-ticker path).
|
|
app.command(name="download", help="Batch OHLCV download (top-level shortcut for `yfin tickers history`).")(
|
|
download_cmd.download
|
|
)
|
|
|
|
|
|
def _version_callback(value: bool):
|
|
if value:
|
|
typer.echo(f"yfin {__version__}")
|
|
typer.echo(f"yfinance {yf.__version__}")
|
|
raise typer.Exit()
|
|
|
|
|
|
@app.callback()
|
|
def root(
|
|
ctx: typer.Context,
|
|
fmt: Optional[str] = typer.Option(
|
|
None, "--format", "-f", help="Output format: table|json|ndjson|csv|tsv|parquet|yaml.",
|
|
case_sensitive=False,
|
|
),
|
|
out: Optional[Path] = typer.Option(None, "--out", "-o", help="Write to PATH instead of stdout."),
|
|
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON/YAML output."),
|
|
compact: bool = typer.Option(False, "--compact", help="Single-line JSON output."),
|
|
columns: Optional[str] = typer.Option(None, "--columns", help="Comma-separated columns to keep."),
|
|
limit: Optional[int] = typer.Option(None, "--limit", help="Truncate rendered output to N rows."),
|
|
proxy: Optional[str] = typer.Option(None, "--proxy", help="HTTP/HTTPS proxy URL."),
|
|
timeout: float = typer.Option(10.0, "--timeout", help="Request timeout in seconds."),
|
|
retries: int = typer.Option(0, "--retries", help="HTTP retry attempts."),
|
|
lang: str = typer.Option("en-US", "--lang", help="BCP-47 language tag."),
|
|
region: str = typer.Option("US", "--region", help="ISO 3166-1 alpha-2 country code."),
|
|
cache_dir: Optional[str] = typer.Option(None, "--cache-dir", help="Override tz/cookie SQLite cache directory."),
|
|
config_path: Optional[Path] = typer.Option(None, "--config", help="Use a non-default ~/.yfin/config.toml."),
|
|
no_color: bool = typer.Option(False, "--no-color", help="Disable Rich color output."),
|
|
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress progress bars and warnings."),
|
|
verbose: int = typer.Option(0, "--verbose", "-v", count=True, help="-v=INFO, -vv=DEBUG."),
|
|
version: bool = typer.Option(False, "--version", callback=_version_callback, is_eager=True, help="Print versions and exit."),
|
|
):
|
|
"""Set global state on ctx.obj and apply yf.config / cache / cookies before subcommand runs."""
|
|
# Load config file (defaults), then layer flags on top.
|
|
file_cfg = cfg_mod.load(config_path)
|
|
st = GlobalState(
|
|
fmt=fmt,
|
|
out=out,
|
|
pretty=pretty,
|
|
compact=compact,
|
|
columns=columns,
|
|
limit=limit,
|
|
proxy=proxy or file_cfg.proxy,
|
|
timeout=timeout if timeout != 10.0 else file_cfg.timeout,
|
|
retries=retries or file_cfg.retries,
|
|
lang=lang if lang != "en-US" else file_cfg.lang,
|
|
region=region if region != "US" else file_cfg.region,
|
|
cache_dir=cache_dir or file_cfg.cache_dir,
|
|
config_path=config_path,
|
|
no_color=no_color,
|
|
quiet=quiet,
|
|
verbose=verbose,
|
|
cookies_t=file_cfg.cookies_t,
|
|
cookies_y=file_cfg.cookies_y,
|
|
)
|
|
if st.fmt is None and file_cfg.default_format:
|
|
st.fmt = file_cfg.default_format
|
|
|
|
ctx.obj = st
|
|
|
|
# Apply to yfinance global config singleton.
|
|
if st.proxy:
|
|
yf.config.network.proxy = st.proxy
|
|
if st.retries:
|
|
yf.config.network.retries = st.retries
|
|
yf.config.locale.lang = st.lang
|
|
yf.config.locale.region = st.region
|
|
if st.cache_dir:
|
|
yf.set_tz_cache_location(st.cache_dir)
|
|
if st.cookies_t and st.cookies_y:
|
|
try:
|
|
yf.Auth.set_login_cookies(st.cookies_t, st.cookies_y)
|
|
except Exception:
|
|
pass
|
|
|
|
# Logging.
|
|
if st.verbose >= 2:
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
yf.enable_debug_mode()
|
|
elif st.verbose == 1:
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
# Console.
|
|
console = Console(no_color=st.no_color or st.quiet, quiet=False, stderr=False, soft_wrap=False)
|
|
renderers.set_console(console)
|
|
|
|
|
|
def main() -> None:
|
|
"""Console-script entry point. Wraps the Typer app with friendly error handling."""
|
|
try:
|
|
app()
|
|
except YFRateLimitError:
|
|
print(
|
|
"yfin: Yahoo rate-limit (HTTP 429). Your IP has been throttled — wait several minutes, or set --proxy.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
main()
|