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>
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""`yfin config ...` — read / write the yfin config file + in-process yf.config."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import typer
|
|
import yfinance as yf
|
|
|
|
from .._shared import render_via_state, get_state
|
|
from .. import config as cfg_mod
|
|
|
|
|
|
app = typer.Typer(no_args_is_help=True, help="yfin / yfinance config.")
|
|
|
|
|
|
@app.command("get")
|
|
def get(
|
|
ctx: typer.Context,
|
|
key: Optional[str] = typer.Argument(None, help='Optional dotted path, e.g. "network.proxy".'),
|
|
):
|
|
"""Dump the active yf.config tree (defaults + flags applied)."""
|
|
raw = json.loads(repr(yf.config))
|
|
if key is None:
|
|
render_via_state(ctx, raw)
|
|
return
|
|
val = raw
|
|
for part in key.split("."):
|
|
if not isinstance(val, dict) or part not in val:
|
|
raise typer.BadParameter(f"Key {key!r} not found.")
|
|
val = val[part]
|
|
render_via_state(ctx, val)
|
|
|
|
|
|
@app.command("set")
|
|
def set_cmd(
|
|
ctx: typer.Context,
|
|
proxy: Optional[str] = typer.Option(None, "--proxy"),
|
|
retries: Optional[int] = typer.Option(None, "--retries"),
|
|
lang: Optional[str] = typer.Option(None, "--lang"),
|
|
region: Optional[str] = typer.Option(None, "--region"),
|
|
hide_exceptions: Optional[bool] = typer.Option(None, "--hide-exceptions/--no-hide-exceptions"),
|
|
logging_on: Optional[bool] = typer.Option(None, "--logging/--no-logging"),
|
|
save: bool = typer.Option(False, "--save/--no-save", help="Persist to ~/.yfin/config.toml."),
|
|
):
|
|
"""Update yf.config (and optionally persist to ~/.yfin/config.toml)."""
|
|
if proxy is not None:
|
|
yf.config.network.proxy = proxy or None
|
|
if retries is not None:
|
|
yf.config.network.retries = retries
|
|
if lang is not None:
|
|
yf.config.locale.lang = lang
|
|
if region is not None:
|
|
yf.config.locale.region = region
|
|
if hide_exceptions is not None:
|
|
yf.config.debug.hide_exceptions = hide_exceptions
|
|
if logging_on is not None:
|
|
yf.config.debug.logging = logging_on
|
|
|
|
if save:
|
|
st = get_state(ctx)
|
|
cfg = cfg_mod.load(st.config_path)
|
|
if proxy is not None:
|
|
cfg.proxy = proxy or None
|
|
if retries is not None:
|
|
cfg.retries = retries
|
|
if lang is not None:
|
|
cfg.lang = lang
|
|
if region is not None:
|
|
cfg.region = region
|
|
p = cfg_mod.save(cfg, st.config_path)
|
|
typer.echo(f"Saved to {p}.")
|
|
render_via_state(ctx, json.loads(repr(yf.config)))
|
|
|
|
|
|
@app.command("debug-on")
|
|
def debug_on(ctx: typer.Context):
|
|
"""Enable verbose yfinance logging (yf.enable_debug_mode())."""
|
|
yf.enable_debug_mode()
|
|
typer.echo("Debug logging enabled.")
|
|
|
|
|
|
@app.command("cache-dir")
|
|
def cache_dir(ctx: typer.Context, path: Path = typer.Argument(...)):
|
|
"""Point yfinance's SQLite tz/cookie cache at PATH."""
|
|
yf.set_tz_cache_location(str(path))
|
|
typer.echo(f"Cache directory set to {path}.")
|
|
|
|
|
|
@app.command("clear-cache")
|
|
def clear_cache(
|
|
ctx: typer.Context,
|
|
path: Optional[Path] = typer.Option(None, "--path", help="Directory to remove; default = active cache."),
|
|
):
|
|
"""Delete the on-disk tz/cookie cache directory."""
|
|
import platformdirs
|
|
import shutil
|
|
|
|
target = path or Path(platformdirs.user_cache_dir()) / "py-yfinance"
|
|
if target.exists():
|
|
shutil.rmtree(target)
|
|
typer.echo(f"Removed {target}.")
|
|
else:
|
|
typer.echo(f"Nothing to remove at {target}.")
|