Spaces:
Sleeping
Sleeping
Download app.py from eshan6704/marketapi2: direct link, hf CLI and curl.
- Browser
- Download file 34.8 kB
-
https://huggingface.co/spaces/eshan6704/marketapi2/resolve/main/app.py
- Command line
-
hf download hf://spaces/eshan6704/marketapi2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/eshan6704/marketapi2/resolve/main/app.py
34.8 kB
| """ | |
| app.py β MarketAPI2 Flask Server (COMPLETE v2 + FastInfo) | |
| All market.py + yahoo_extended.py functions + FastInfo snapshot endpoints. | |
| Public Space: eshan6704/marketapi2 on HuggingFace | |
| CSV-only: every endpoint returns text/csv. There is no JSON output anywhere | |
| in this file (no jsonify, no Flask JSON provider) β status/metadata | |
| endpoints that used to be small JSON dicts (health, endpoints list, | |
| fastinfo groups/refresh, error responses) are now flattened into | |
| key,value CSV via dict_to_csv(), and tabular endpoints go through | |
| market.py's md.to_csv() as before. | |
| """ | |
| from flask import Flask, request, Response, render_template, redirect | |
| import market as md | |
| import market as yf_ext | |
| import logging | |
| import pandas as pd | |
| import math | |
| from datetime import datetime | |
| app = Flask(__name__, template_folder="templates") | |
| # FastInfo keeps its own hourly background refresh thread (the one | |
| # exception to "on-demand only" β every other NSE/Yahoo function in | |
| # market.py is strictly request-triggered). Live/on-demand refreshes via | |
| # /fastinfo/refresh* share the same 1-minute floor as the background loop. | |
| md.start_background() | |
| # βββββββββββββββββββββββββββ DICT-TO-CSV (status/meta endpoints) ββββββββββ | |
| def _clean_csv_value(v): | |
| """NaN/Inf -> blank, lists/tuples -> '; '-joined string, everything | |
| else passed through as-is for csv.writer to stringify.""" | |
| if v is None: | |
| return "" | |
| if isinstance(v, float) and (math.isnan(v) or math.isinf(v)): | |
| return "" | |
| if isinstance(v, (list, tuple)): | |
| return "; ".join(str(_clean_csv_value(x)) for x in v) | |
| return v | |
| def _flatten_dict(d, parent_key=""): | |
| """Flatten a nested dict into dot-path -> value pairs, in insertion | |
| order. E.g. {"a": {"b": 1}} -> {"a.b": 1}. Lists are left as lists | |
| (joined into a single cell later by _clean_csv_value) rather than | |
| exploded into indexed keys β keeps small status dicts (module lists, | |
| endpoint groups) readable as one row instead of a.0, a.1, a.2...""" | |
| items = {} | |
| for k, v in d.items(): | |
| key = f"{parent_key}.{k}" if parent_key else str(k) | |
| if isinstance(v, dict): | |
| items.update(_flatten_dict(v, key)) | |
| else: | |
| items[key] = v | |
| return items | |
| def dict_to_csv(d: dict) -> str: | |
| """ | |
| Flatten a (possibly nested) status/metadata dict into two-column | |
| 'key,value' CSV β used for every endpoint that used to return a small | |
| JSON dict (health, endpoint listing, fastinfo refresh confirmations, | |
| error responses) rather than a market-data table. | |
| """ | |
| flat = _flatten_dict(d) | |
| df = pd.DataFrame( | |
| [{"key": k, "value": _clean_csv_value(v)} for k, v in flat.items()] | |
| ) | |
| return md.to_csv(df) | |
| def csv_status_response(data: dict, status_code: int = 200, filename: str = "status.csv") -> Response: | |
| """dict_to_csv, wrapped in a Response with the right status code β | |
| the CSV-only equivalent of the old safe_jsonify().""" | |
| csv_string = dict_to_csv(data) | |
| resp = Response( | |
| csv_string, | |
| mimetype="text/csv", | |
| headers={"Content-Disposition": f"attachment;filename={filename}"}, | |
| ) | |
| resp.status_code = status_code | |
| return resp | |
| def records_to_csv(rows: list, filename: str = "data.csv") -> Response: | |
| """List of flat dicts (e.g. one row per symbol/group) -> CSV Response. | |
| Used for the fastinfo endpoints, which are genuinely tabular (many | |
| symbols/groups sharing the same fields) rather than a single status | |
| dict, so they get a real table instead of a key,value dump.""" | |
| if not rows: | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| df = pd.DataFrame(rows) | |
| return csv_response(md.to_csv(df), filename) | |
| # βββββββββββββββββββββββββββ LOGGING ββββββββββββββββββββββββββββββββββββββ | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s β %(message)s", | |
| ) | |
| log = logging.getLogger(__name__) | |
| # βββββββββββββββββββββββββββ HELPERS ββββββββββββββββββββββββββββββββββββββ | |
| def csv_response(csv_string: str, filename: str = "data.csv") -> Response: | |
| """Wrap CSV string in Flask Response with proper headers.""" | |
| if not csv_string: | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| return Response( | |
| csv_string, | |
| mimetype="text/csv", | |
| headers={"Content-Disposition": f"attachment;filename={filename}"} | |
| ) | |
| def safe_csv(func, filename: str, *args, **kwargs): | |
| """Safely call a function and return CSV response.""" | |
| try: | |
| result = func(*args, **kwargs) | |
| if isinstance(result, dict) and all(isinstance(v, pd.DataFrame) for v in result.values()): | |
| for k, v in result.items(): | |
| if not v.empty: | |
| csv = md.to_csv(v) | |
| return csv_response(csv, f"{filename.rsplit('.', 1)[0]}_{k}.csv") | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| csv = md.to_csv(result) | |
| return csv_response(csv, filename) | |
| except Exception as e: | |
| log.error("Error in %s: %s", func.__name__, e) | |
| return csv_status_response({"error": str(e)}, 500) | |
| def want_raw() -> bool: | |
| """ | |
| Whether the caller wants unformatted raw numbers instead of the default | |
| Lakh/Crore human-readable formatting. Opt-in via ?raw=true β every | |
| endpoint that supports it defaults to human_readable=True, since the | |
| whole point of a human-facing CSV is not showing "782132800000.0". | |
| """ | |
| return request.args.get("raw", "false").strip().lower() == "true" | |
| def respond(func, filename, symbol=None, *args, **kwargs): | |
| """ | |
| Call func and return a CSV response. `symbol` is accepted for call-site | |
| compatibility (every /api/* route still passes it) but no longer used β | |
| CSV-only now, no format negotiation. | |
| """ | |
| return safe_csv(func, filename, *args, **kwargs) | |
| def stack_for_csv(result: dict, skip=("symbol",)) -> pd.DataFrame: | |
| """ | |
| Combine a dict of tables (DataFrames and/or plain dicts) into one flat | |
| DataFrame for CSV export, tagged with a leading '_source' column so each | |
| original table stays identifiable after stacking. Used for endpoints | |
| that return several small related tables (holders, estimates, analyst | |
| snapshot, calendar+esg, financial statements) β CSV can't hold nested | |
| structure, so this keeps everything in one file instead of only the | |
| first non-empty table. | |
| """ | |
| frames = [] | |
| for key, val in result.items(): | |
| if key in skip: | |
| continue | |
| if isinstance(val, pd.DataFrame): | |
| df = val | |
| elif isinstance(val, dict): | |
| df = pd.DataFrame([val]) | |
| else: | |
| continue | |
| if df.empty: | |
| continue | |
| df = df.copy() | |
| df.insert(0, "_source", key) | |
| frames.append(df) | |
| if not frames: | |
| return pd.DataFrame() | |
| return pd.concat(frames, ignore_index=True, sort=False) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HEALTH & INFO | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| """Serve the merged FastInfo + API Console page (single-page, tab-switched).""" | |
| return render_template("index.html") | |
| def health(): | |
| """Health check endpoint.""" | |
| return csv_status_response({ | |
| "status": "ok", | |
| "space": "eshan6704/marketapi2", | |
| "modules": ["market.py"], | |
| "endpoints": { | |
| "data": "/api/* (CSV)", | |
| "ui": "/ (single page: Overview tab + Console tab)" | |
| }, | |
| "network": md.network_stats(), | |
| }, filename="health.csv") | |
| def clear_cache(): | |
| """Manually flush the in-memory TTL cache (e.g. after a known bad/stale fetch).""" | |
| md._cache.clear() | |
| return csv_status_response({"status": "cache cleared"}, filename="cache_clear.csv") | |
| def list_endpoints(): | |
| """List all available endpoints as a category,endpoint,pattern CSV table.""" | |
| endpoints = { | |
| "nse": { | |
| "bhavcopy": "/api/bhavcopy/<date>", | |
| "bulkdeals": "/api/bulkdeals", | |
| "blockdeals": "/api/blockdeals", | |
| "equity_volatility": "/api/equity_volatility/<date>", | |
| "fo_volatility": "/api/fo_volatility/<date>", | |
| "fo_bhavcopy": "/api/fo_bhavcopy/<date>", | |
| "fo_secban": "/api/fo_secban", | |
| "indices_bhavcopy": "/api/indices_bhavcopy/<date>", | |
| "pr_bhavcopy_bundle": "/api/pr_bhavcopy_bundle/<date>", | |
| "insider_trading": "/api/insider_trading?symbol=TCS&from_date=DD-MM-YYYY&to_date=DD-MM-YYYY", | |
| "sast_disclosures": "/api/sast_disclosures?symbol=TCS&from_date=DD-MM-YYYY&to_date=DD-MM-YYYY", | |
| "pledged_shares": "/api/pledged_shares?symbol=TCS&from_date=DD-MM-YYYY&to_date=DD-MM-YYYY", | |
| "nse_circulars": "/api/nse_circulars?from_date=DD-MM-YYYY&to_date=DD-MM-YYYY&dept=CD", | |
| "market_status": "/api/market_status", | |
| "preopen": "/api/preopen?key=NIFTY|BANKNIFTY|FO|SME|ALL", | |
| "most_active": "/api/most_active?type=securities|etf&sort=value|volume", | |
| "gainers_losers": "/api/gainers_losers?index=NIFTY&category=gainers|losers", | |
| "price_band_hitters": "/api/price_band_hitters?band=both&view=AllSec", | |
| "circuit_breakers": "/api/circuit_breakers", | |
| "sector_watch": "/api/sector_watch", | |
| "indices": "/api/indices?filter=true|false", | |
| "index_constituents": "/api/index_constituents?index=NIFTY%2050&noofrecords=0", | |
| "index_pe_pb": "/api/index_pe_pb?index_name=NIFTY%2050", | |
| "index_hist": "/api/index_hist?index_name=NIFTY%2050&start=DD-MM-YYYY&end=DD-MM-YYYY", | |
| "stock_hist": "/api/stock_hist/<symbol>?start=DD-MM-YYYY&end=DD-MM-YYYY&series=EQ", | |
| "index_chain": "/api/index_chain/<symbol>?expiry=DD-MM-YYYY", | |
| "stock_chain": "/api/stock_chain/<symbol>?expiry=DD-MM-YYYY", | |
| "oi_spurts": "/api/oi_spurts?mode=oi_spurts|oi_decrease|price_volume_spurts", | |
| "fno_lot_sizes": "/api/fno_lot_sizes", | |
| "events": "/api/events", | |
| "nse_results": "/api/nse_results?index=equities|debt|sme&period=Quarterly|Annual|Half-Yearly|Others", | |
| "nse_holidays": "/api/nse_holidays?type=trading|clearing", | |
| "fiidii": "/api/fiidii", | |
| "delivery_heatmap": "/api/delivery_heatmap/<date>", | |
| "largedeals": "/api/largedeals?mode=bulk_deals|block_deals|short_deals", | |
| "eq_symbols": "/api/eq_symbols", | |
| "highlow": "/api/highlow/<date>", | |
| "stock_highlow": "/api/stock_highlow/<date>", | |
| "today_str": "/api/today_str?fmt=%d-%m-%Y", | |
| "last_trading_day": "/api/last_trading_day", | |
| }, | |
| "yahoo": { | |
| "info": "/api/yf_info/<symbol>", | |
| "history": "/api/yf_history/<symbol>?period=1y&interval=1d", | |
| "intraday": "/api/yf_intraday/<symbol>?interval=5m&period=5d", | |
| "daily_trend": "/api/yf_daily_trend/<symbol>?period=1mo", | |
| "actions": "/api/yf_actions/<symbol> (dividends/splits/capital_gains are rows in this table)", | |
| "income_stmt": "/api/yf_income_stmt/<symbol>?quarterly=false", | |
| "balance_sheet": "/api/yf_balance_sheet/<symbol>?quarterly=false", | |
| "cashflow": "/api/yf_cashflow/<symbol>?quarterly=false", | |
| "all_financials": "/api/yf_all_financials/<symbol> (all 6 statements stacked, _source column)", | |
| "earnings": "/api/yf_earnings/<symbol>?quarterly=false", | |
| "estimates": "/api/yf_estimates/<symbol> (all 5 estimate tables stacked, _source column)", | |
| "recommendations": "/api/yf_recommendations/<symbol>", | |
| "analyst": "/api/yf_analyst/<symbol> (JSON: price_targets/summary/upgrades_downgrades as separate tables; CSV: all stacked with _source column)", | |
| "holders": "/api/yf_holders/<symbol> (JSON: major/institutional/mutualfund/insider_* as separate tables; CSV: all stacked with _source column)", | |
| }, | |
| "fastinfo": { | |
| "dashboard": "/ (merged into the single page's Overview tab)", | |
| "snapshot": "/fastinfo/snapshot", | |
| "group_snapshot": "/fastinfo/snapshot/<group>", | |
| "refresh_group": "POST /fastinfo/refresh/<group>", | |
| "refresh_all": "POST /fastinfo/refresh-all", | |
| "groups": "/fastinfo/groups", | |
| } | |
| } | |
| rows = [ | |
| {"category": category, "endpoint": name, "pattern": pattern} | |
| for category, group in endpoints.items() | |
| for name, pattern in group.items() | |
| ] | |
| return records_to_csv(rows, filename="endpoints.csv") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # UNIFIED API ENDPOINTS (CSV only) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def api_bhavcopy(date): | |
| return respond(md.bhavcopy, f"bhavcopy_{date}.csv", None, date) | |
| def api_bulkdeals(): | |
| return respond(md.bulkdeals, "bulkdeals.csv", None, not want_raw()) | |
| def api_blockdeals(): | |
| return respond(md.blockdeals, "blockdeals.csv", None, not want_raw()) | |
| def api_equity_volatility(date): | |
| return respond(md.equity_volatility, f"equity_volatility_{date}.csv", None, date) | |
| def api_fo_volatility(date): | |
| return respond(md.fo_volatility, f"fo_volatility_{date}.csv", None, date) | |
| def api_fo_bhavcopy(date): | |
| return respond(md.fo_bhavcopy, f"fo_bhavcopy_{date}.csv", None, date) | |
| def api_fo_secban(): | |
| return respond(md.fo_secban, "fo_secban.csv", None) | |
| def api_indices_bhavcopy(date): | |
| return respond(md.indices_bhavcopy, f"indices_bhavcopy_{date}.csv", None, date) | |
| def api_pr_bhavcopy_bundle(date): | |
| return respond(md.pr_bhavcopy_bundle, f"pr_bhavcopy_bundle_{date}.csv", None, date) | |
| def api_insider_trading(): | |
| symbol = request.args.get("symbol") | |
| from_date = request.args.get("from_date") | |
| to_date = request.args.get("to_date") | |
| return respond(md.insider_trading, "insider_trading.csv", symbol, symbol, from_date, to_date, not want_raw()) | |
| def api_sast_disclosures(): | |
| symbol = request.args.get("symbol") | |
| from_date = request.args.get("from_date") | |
| to_date = request.args.get("to_date") | |
| return respond(md.sast_disclosures, "sast_disclosures.csv", symbol, symbol, from_date, to_date, not want_raw()) | |
| def api_pledged_shares(): | |
| symbol = request.args.get("symbol") | |
| from_date = request.args.get("from_date") | |
| to_date = request.args.get("to_date") | |
| return respond(md.pledged_shares, "pledged_shares.csv", symbol, symbol, from_date, to_date, not want_raw()) | |
| def api_nse_circulars(): | |
| from_date = request.args.get("from_date") | |
| to_date = request.args.get("to_date") | |
| dept = request.args.get("dept") | |
| return respond(md.nse_circulars, "nse_circulars.csv", None, from_date, to_date, dept) | |
| def api_market_status(): | |
| return respond(md.market_status, "market_status.csv", None, not want_raw()) | |
| def api_preopen(): | |
| key = request.args.get("key", "NIFTY") | |
| return respond(md.preopen, f"preopen_{key}.csv", None, key, not want_raw()) | |
| def api_most_active(): | |
| type_ = request.args.get("type", "securities") | |
| sort = request.args.get("sort", "value") | |
| return respond(md.most_active, f"most_active_{type_}_{sort}.csv", None, type_, sort, not want_raw()) | |
| def api_gainers_losers(): | |
| index = request.args.get("index", "NIFTY") | |
| category = request.args.get("category", "gainers") | |
| return respond(md.gainers_losers, f"{category}_{index}.csv", None, index, category, not want_raw()) | |
| def api_price_band_hitters(): | |
| band = request.args.get("band", "both") | |
| view = request.args.get("view", "AllSec") | |
| return respond(md.price_band_hitters, f"price_band_{band}_{view}.csv", None, band, view, not want_raw()) | |
| def api_circuit_breakers(): | |
| return respond(md.circuit_breakers, "circuit_breakers.csv", None, not want_raw()) | |
| def api_sector_watch(): | |
| return respond(md.sector_watch, "sector_watch.csv", None, not want_raw()) | |
| def api_indices(): | |
| filter_to_default = request.args.get("filter", "true").lower() == "true" | |
| return respond(md.indices, "indices.csv", None, filter_to_default) | |
| def api_index_constituents(): | |
| """Live constituent stocks for an index. ?index=NIFTY%2050&noofrecords=0""" | |
| index_name = request.args.get("index", "NIFTY 50") | |
| noofrecords = int(request.args.get("noofrecords", "0")) | |
| return respond( | |
| md.index_constituents, | |
| f"constituents_{index_name.replace(' ', '_')}.csv", | |
| None, index_name, noofrecords | |
| ) | |
| def api_index_pe_pb(): | |
| index_name = request.args.get("index_name", "NIFTY 50") | |
| return respond(md.index_pe_pb, f"index_pe_pb_{index_name.replace(' ', '_')}.csv", None, index_name) | |
| def api_index_hist(): | |
| index_name = request.args.get("index_name", "NIFTY 50") | |
| start = request.args.get("start") | |
| end = request.args.get("end") | |
| if not start or not end: | |
| return csv_status_response({"error": "Required: ?start=DD-MM-YYYY&end=DD-MM-YYYY"}, 400) | |
| return respond(md.index_hist, f"index_hist_{index_name.replace(' ', '_')}_{start}_{end}.csv", None, index_name, start, end) | |
| def api_stock_hist(symbol): | |
| start = request.args.get("start") | |
| end = request.args.get("end") | |
| series = request.args.get("series", "EQ") | |
| if not start or not end: | |
| return csv_status_response({"error": "Required: ?start=DD-MM-YYYY&end=DD-MM-YYYY"}, 400) | |
| try: | |
| return respond(md.stock_hist, f"hist_{symbol}_{start}_{end}.csv", symbol, start, end, symbol, series) | |
| except ValueError as e: | |
| return csv_status_response({"error": str(e)}, 400) | |
| def api_index_chain(symbol): | |
| expiry = request.args.get("expiry") | |
| if not expiry: | |
| return csv_status_response({"error": "Required: ?expiry=DD-MM-YYYY"}, 400) | |
| return respond(md.index_chain, f"index_chain_{symbol}_{expiry}.csv", symbol, symbol, expiry) | |
| def api_stock_chain(symbol): | |
| expiry = request.args.get("expiry") | |
| if not expiry: | |
| return csv_status_response({"error": "Required: ?expiry=DD-MM-YYYY"}, 400) | |
| return respond(md.stock_chain, f"stock_chain_{symbol}_{expiry}.csv", symbol, symbol, expiry) | |
| def api_oi_spurts(): | |
| mode = request.args.get("mode", "oi_spurts") | |
| return respond(md.oi_spurts, f"oi_{mode}.csv", None, mode) | |
| def api_fno_lot_sizes(): | |
| return respond(md.fno_lot_sizes, "fno_lot_sizes.csv", None) | |
| def api_events(): | |
| return respond(md.events, "events.csv", None) | |
| def api_nse_results(): | |
| index = request.args.get("index", "equities") | |
| period = request.args.get("period", "Quarterly") | |
| try: | |
| return respond(md.nse_results, f"results_{index}_{period}.csv", None, index, period, not want_raw()) | |
| except ValueError as e: | |
| return csv_status_response({"error": str(e)}, 400) | |
| def api_nse_holidays(): | |
| type_ = request.args.get("type", "trading") | |
| return respond(md.nse_holidays, f"holidays_{type_}.csv", None, type_) | |
| def api_fiidii(): | |
| return respond(md.fiidii, "fiidii.csv", None, not want_raw()) | |
| def api_delivery_heatmap(date): | |
| return respond(md.delivery_heatmap, f"delivery_heatmap_{date}.csv", None, date) | |
| def api_largedeals(): | |
| mode = request.args.get("mode", "bulk_deals") | |
| return respond(md.largedeals, f"{mode}.csv", None, mode, not want_raw()) | |
| def api_eq_symbols(): | |
| return respond(md.eq_symbols, "eq_symbols.csv", None) | |
| def api_highlow(date_str): | |
| return respond(md.highlow, f"highlow_{date_str}.csv", None, date_str) | |
| def api_stock_highlow(date_str): | |
| return respond(md.stock_highlow, f"stock_highlow_{date_str}.csv", None, date_str) | |
| def api_today_str(): | |
| fmt = request.args.get("fmt", "%d-%m-%Y") | |
| return respond(md.today_str, "today.csv", None, fmt) | |
| def api_last_trading_day(): | |
| return respond(md.last_trading_day, "last_trading_day.csv", None) | |
| def api_yf_info(symbol): | |
| try: | |
| result = yf_ext.yf_info(symbol) | |
| return csv_response(md.to_csv(result), f"yf_info_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 500) | |
| def api_yf_history(symbol): | |
| period = request.args.get("period", "1y") | |
| interval = request.args.get("interval", "1d") | |
| start = request.args.get("start") | |
| end = request.args.get("end") | |
| prepost = request.args.get("prepost", "false").lower() == "true" | |
| try: | |
| df = yf_ext.yf_history(symbol, period, interval, start, end, prepost) | |
| if df is None or df.empty: | |
| return csv_status_response({"symbol": symbol, "error": "No data"}, 404) | |
| return csv_response(md.to_csv(df), f"yf_history_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 400) | |
| def api_yf_intraday(symbol): | |
| interval = request.args.get("interval", "5m") | |
| period = request.args.get("period", "5d") | |
| prepost = request.args.get("prepost", "false").lower() == "true" | |
| try: | |
| df = yf_ext.yf_intraday(symbol, interval, period, prepost) | |
| if df is None or df.empty: | |
| return csv_status_response({"symbol": symbol, "error": "No data"}, 404) | |
| return csv_response(md.to_csv(df), f"yf_intraday_{symbol}_{interval}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 400) | |
| def api_yf_daily_trend(symbol): | |
| period = request.args.get("period", "1mo") | |
| try: | |
| df = yf_ext.yf_daily_trend(symbol, period) | |
| if df is None or df.empty: | |
| return csv_status_response({"symbol": symbol, "error": "No data"}, 404) | |
| return csv_response(md.to_csv(df), f"yf_trend_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 400) | |
| def api_yf_actions(symbol): | |
| return respond(yf_ext.yf_actions, f"yf_actions_{symbol}.csv", symbol, symbol) | |
| def api_yf_income_stmt(symbol): | |
| quarterly = request.args.get("quarterly", "false").lower() == "true" | |
| return respond(yf_ext.yf_income_stmt, f"yf_income_{symbol}.csv", symbol, symbol, quarterly, not want_raw()) | |
| def api_yf_balance_sheet(symbol): | |
| quarterly = request.args.get("quarterly", "false").lower() == "true" | |
| return respond(yf_ext.yf_balance_sheet, f"yf_balance_{symbol}.csv", symbol, symbol, quarterly, not want_raw()) | |
| def api_yf_cashflow(symbol): | |
| quarterly = request.args.get("quarterly", "false").lower() == "true" | |
| return respond(yf_ext.yf_cashflow, f"yf_cashflow_{symbol}.csv", symbol, symbol, quarterly, not want_raw()) | |
| def api_yf_all_financials(symbol): | |
| """Stacks all 6 statement tables into one CSV file (tagged by _source column).""" | |
| try: | |
| result = yf_ext.yf_all_financials(symbol, human_readable=not want_raw()) | |
| combined = stack_for_csv(result) | |
| if combined.empty: | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| return csv_response(md.to_csv(combined), f"yf_financials_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 500) | |
| def api_yf_earnings(symbol): | |
| quarterly = request.args.get("quarterly", "false").lower() == "true" | |
| return respond(yf_ext.yf_earnings, f"yf_earnings_{symbol}.csv", symbol, symbol, quarterly) | |
| def api_yf_estimates(symbol): | |
| """Stacks all 5 estimate tables into one CSV file (tagged by _source column).""" | |
| try: | |
| result = yf_ext.yf_estimates(symbol) | |
| combined = stack_for_csv(result) | |
| if combined.empty: | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| return csv_response(md.to_csv(combined), f"yf_estimates_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 500) | |
| def api_yf_recommendations(symbol): | |
| return respond(yf_ext.yf_recommendations, f"yf_recs_{symbol}.csv", symbol, symbol) | |
| def api_yf_analyst(symbol): | |
| """Stacks price_targets + summary + upgrades_downgrades into one CSV file (tagged by _source column).""" | |
| try: | |
| result = yf_ext.yf_analyst(symbol) | |
| combined = stack_for_csv(result) | |
| if combined.empty: | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| return csv_response(md.to_csv(combined), f"yf_analyst_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 500) | |
| def api_yf_holders(symbol): | |
| """Stacks all 6 holder tables into one CSV file (tagged by _source column).""" | |
| try: | |
| result = yf_ext.yf_holders(symbol) | |
| combined = stack_for_csv(result) | |
| if combined.empty: | |
| return Response("No data available", status=204, mimetype="text/plain") | |
| return csv_response(md.to_csv(combined), f"yf_holders_{symbol}.csv") | |
| except Exception as e: | |
| return csv_status_response({"error": str(e)}, 500) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FASTINFO ENDPOINTS (Market Snapshot + On-Demand Refresh) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fastinfo_page(): | |
| """Legacy URL β FastInfo is now merged into the single page at '/'.""" | |
| return redirect("/") | |
| def _snapshot_group_rows(group_name: str, group_payload: dict) -> list: | |
| """Flatten one group's {"data": {symbol: {price, change_pct, ...}}, "failed": [...]} | |
| payload into a list of flat row dicts, one per symbol β this is what | |
| actually makes fastinfo's output a real table instead of a status dump.""" | |
| data = group_payload.get("data", {}) if isinstance(group_payload, dict) else {} | |
| failed = set(group_payload.get("failed", [])) if isinstance(group_payload, dict) else set() | |
| rows = [] | |
| for sym, fields in data.items(): | |
| row = {"group": group_name, "symbol": sym} | |
| if isinstance(fields, dict): | |
| row.update(fields) | |
| row["status"] = "failed" if sym in failed else "ok" | |
| rows.append(row) | |
| return rows | |
| def fastinfo_snapshot(): | |
| """Full snapshot as one CSV table (group, symbol, price, change_pct, | |
| currency, timestamp, status). Kept warm by FastInfo's hourly background | |
| thread β this just reads it. For a guaranteed-fresh ("live") read, use | |
| POST /fastinfo/refresh-all instead (min 1 minute between real fetches).""" | |
| snap = md.load_snapshot() | |
| if not snap: | |
| return csv_status_response({"error": "Snapshot not ready yet"}, 503) | |
| rows = [] | |
| for group_name, payload in snap.items(): | |
| if group_name == "_meta": | |
| continue | |
| rows.extend(_snapshot_group_rows(group_name, payload)) | |
| return records_to_csv(rows, filename="fastinfo_snapshot.csv") | |
| def fastinfo_group_snapshot(group): | |
| """One group's cached data as a CSV table. See /fastinfo/refresh/<group> for a live read.""" | |
| data = md.get_group_data(group) | |
| if not data: | |
| return csv_status_response({"error": f"Group '{group}' not found"}, 404) | |
| rows = _snapshot_group_rows(group, data) | |
| return records_to_csv(rows, filename=f"fastinfo_{group}.csv") | |
| def fastinfo_refresh_group(group): | |
| """ | |
| INSTANT REFRESH for a specific group. | |
| Fetches live data, updates snapshot, returns fresh data as a CSV table. | |
| """ | |
| if group not in md.MARKET_SYMBOLS: | |
| return csv_status_response({ | |
| "error": f"Group '{group}' not found", | |
| "available": list(md.MARKET_SYMBOLS.keys()) | |
| }, 404) | |
| result = md.refresh_group(group) | |
| rows = _snapshot_group_rows(group, result) | |
| return records_to_csv(rows, filename=f"fastinfo_{group}_refreshed.csv") | |
| def fastinfo_refresh_all(): | |
| """Manually trigger full refresh (all groups).""" | |
| md.refresh_all() | |
| return csv_status_response({ | |
| "status": "refreshed", | |
| "groups": list(md.MARKET_SYMBOLS.keys()), | |
| "timestamp": datetime.utcnow().isoformat(), | |
| }, filename="refresh_status.csv") | |
| def fastinfo_groups(): | |
| """List all groups with symbol counts and actual refresh cadence, as a CSV table. | |
| Every group shares the same real cadence: refreshed by the hourly | |
| background thread, or sooner on demand via /fastinfo/refresh*, subject | |
| to a shared 1-minute floor between real Yahoo fetches. FETCH_INTERVALS | |
| (per-group values) is legacy config not currently wired to a scheduler, | |
| so it's intentionally not reported here to avoid implying a cadence | |
| that doesn't exist. | |
| """ | |
| rows = [ | |
| { | |
| "group": group, | |
| "symbol_count": len(symbols), | |
| "background_refresh_sec": md.REFRESH_INTERVAL, | |
| "min_ondemand_refresh_sec": md.MIN_REFRESH_COOLDOWN, | |
| "symbols": symbols, | |
| } | |
| for group, symbols in md.MARKET_SYMBOLS.items() | |
| ] | |
| return records_to_csv(rows, filename="fastinfo_groups.csv") | |
| # βββββββββββββββββββββββββββ ERROR HANDLERS ββββββββββββββββββββββββββββββ | |
| from werkzeug.exceptions import HTTPException | |
| def handle_http_exception(error): | |
| """Catch-all for every HTTP error Flask/Werkzeug can raise (400, 404, | |
| 405, 500, ...) β not just the two explicitly triggered by app logic β | |
| so no error path anywhere in this app can fall back to Flask's default | |
| JSON/HTML error page.""" | |
| if error.code and error.code >= 500: | |
| log.error("Internal error: %s", error) | |
| return csv_status_response( | |
| {"error": error.name, "message": error.description}, | |
| error.code or 500, | |
| filename="error.csv", | |
| ) | |
| def not_found(error): | |
| return csv_status_response({"error": "Endpoint not found", "message": str(error)}, 404, filename="error.csv") | |
| def internal_error(error): | |
| log.error("Internal error: %s", error) | |
| return csv_status_response({"error": "Internal server error"}, 500, filename="error.csv") | |
| # βββββββββββββββββββββββββββ STARTUP ββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| log.info("Starting MarketAPI2 (Complete v2 + FastInfo) on port 7860") | |
| app.run(debug=False, host="0.0.0.0", port=7860) | |