Files
yfinance-fork/cli/yfin/__main__.py
T
goldyard 057bc4493e Add yfin CLI + tool catalog + CLAUDE.md
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>
2026-06-08 23:36:56 +08:00

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()