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>
67 lines
1.8 KiB
Python
67 lines
1.8 KiB
Python
"""Shared CLI plumbing: global options state + helpers used by every command."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import List, Optional, Union
|
|
|
|
import typer
|
|
|
|
|
|
@dataclass
|
|
class GlobalState:
|
|
"""Snapshot of root-level flags. Stored on Typer context.obj."""
|
|
fmt: Optional[str] = None
|
|
out: Optional[Path] = None
|
|
pretty: Optional[bool] = None
|
|
compact: bool = False
|
|
columns: Optional[str] = None
|
|
limit: Optional[int] = None
|
|
proxy: Optional[str] = None
|
|
timeout: float = 10.0
|
|
retries: int = 0
|
|
lang: str = "en-US"
|
|
region: str = "US"
|
|
cache_dir: Optional[str] = None
|
|
config_path: Optional[Path] = None
|
|
no_color: bool = False
|
|
quiet: bool = False
|
|
verbose: int = 0
|
|
cookies_t: Optional[str] = None
|
|
cookies_y: Optional[str] = None
|
|
config_extras: dict = field(default_factory=dict)
|
|
|
|
|
|
def get_state(ctx: typer.Context) -> GlobalState:
|
|
return ctx.obj # set in __main__.py root callback
|
|
|
|
|
|
def render_via_state(ctx: typer.Context, value) -> None:
|
|
"""Render with the global --format / --out / --pretty / --columns / --limit."""
|
|
from . import renderers
|
|
|
|
st = get_state(ctx)
|
|
pretty = None
|
|
if st.pretty:
|
|
pretty = True
|
|
elif st.compact:
|
|
pretty = False
|
|
renderers.render(
|
|
value,
|
|
fmt=st.fmt,
|
|
out=st.out,
|
|
pretty=pretty,
|
|
columns=st.columns,
|
|
limit=st.limit,
|
|
)
|
|
|
|
|
|
def parse_symbols(arg: Union[str, List[str]]) -> List[str]:
|
|
"""Accept '"AAPL MSFT"' or 'AAPL,MSFT' or a real list."""
|
|
if isinstance(arg, list):
|
|
items: List[str] = []
|
|
for v in arg:
|
|
items.extend(v.replace(",", " ").split())
|
|
return [s.upper() for s in items if s]
|
|
return [s.upper() for s in arg.replace(",", " ").split() if s]
|