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>
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""~/.yfin/config.toml loader. Defaults are also flags on the root callback."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
try:
|
|
import tomllib # py311+
|
|
except ModuleNotFoundError: # pragma: no cover
|
|
import tomli as tomllib # type: ignore
|
|
|
|
|
|
DEFAULT_PATH = Path.home() / ".yfin" / "config.toml"
|
|
|
|
|
|
@dataclass
|
|
class AppConfig:
|
|
proxy: Optional[str] = None
|
|
retries: int = 0
|
|
lang: str = "en-US"
|
|
region: str = "US"
|
|
timeout: float = 10.0
|
|
cache_dir: Optional[str] = None
|
|
default_format: Optional[str] = None
|
|
cookies_t: Optional[str] = None
|
|
cookies_y: Optional[str] = None
|
|
extras: dict = field(default_factory=dict)
|
|
|
|
|
|
def load(path: Optional[Path] = None) -> AppConfig:
|
|
p = Path(path) if path else DEFAULT_PATH
|
|
if not p.exists():
|
|
return AppConfig()
|
|
data = tomllib.loads(p.read_text())
|
|
cfg = AppConfig()
|
|
network = data.get("network", {})
|
|
locale = data.get("locale", {})
|
|
cache = data.get("cache", {})
|
|
auth = data.get("auth", {})
|
|
output = data.get("output", {})
|
|
if "proxy" in network:
|
|
cfg.proxy = network["proxy"]
|
|
if "retries" in network:
|
|
cfg.retries = int(network["retries"])
|
|
if "timeout" in network:
|
|
cfg.timeout = float(network["timeout"])
|
|
if "lang" in locale:
|
|
cfg.lang = locale["lang"]
|
|
if "region" in locale:
|
|
cfg.region = locale["region"]
|
|
if "dir" in cache:
|
|
cfg.cache_dir = cache["dir"]
|
|
if "default_format" in output:
|
|
cfg.default_format = output["default_format"]
|
|
if "cookie_t" in auth:
|
|
cfg.cookies_t = auth["cookie_t"]
|
|
if "cookie_y" in auth:
|
|
cfg.cookies_y = auth["cookie_y"]
|
|
cfg.extras = {k: v for k, v in data.items() if k not in {"network", "locale", "cache", "auth", "output"}}
|
|
return cfg
|
|
|
|
|
|
def save(cfg: AppConfig, path: Optional[Path] = None) -> Path:
|
|
p = Path(path) if path else DEFAULT_PATH
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
lines = []
|
|
if cfg.proxy is not None or cfg.retries or cfg.timeout != 10.0:
|
|
lines.append("[network]")
|
|
if cfg.proxy is not None:
|
|
lines.append(f'proxy = "{cfg.proxy}"')
|
|
if cfg.retries:
|
|
lines.append(f"retries = {cfg.retries}")
|
|
if cfg.timeout != 10.0:
|
|
lines.append(f"timeout = {cfg.timeout}")
|
|
lines.append("")
|
|
if cfg.lang != "en-US" or cfg.region != "US":
|
|
lines.append("[locale]")
|
|
lines.append(f'lang = "{cfg.lang}"')
|
|
lines.append(f'region = "{cfg.region}"')
|
|
lines.append("")
|
|
if cfg.cache_dir:
|
|
lines.append("[cache]")
|
|
lines.append(f'dir = "{cfg.cache_dir}"')
|
|
lines.append("")
|
|
if cfg.default_format:
|
|
lines.append("[output]")
|
|
lines.append(f'default_format = "{cfg.default_format}"')
|
|
lines.append("")
|
|
if cfg.cookies_t or cfg.cookies_y:
|
|
lines.append("[auth]")
|
|
if cfg.cookies_t:
|
|
lines.append(f'cookie_t = "{cfg.cookies_t}"')
|
|
if cfg.cookies_y:
|
|
lines.append(f'cookie_y = "{cfg.cookies_y}"')
|
|
lines.append("")
|
|
p.write_text("\n".join(lines).rstrip() + "\n")
|
|
return p
|