Alilnvest-Codal 1.5.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ """
2
+ Alilnvest-Codal
3
+ =========
4
+ کتابخانهٔ مستقل Alilnvest برای کدال -- زنده، بدون لیست ثابت نماد.
5
+
6
+ اصولِ طراحی (حاصلِ یه بررسیِ چندمرحله‌ای رویِ منابع پژوهشی قبلی و کتابخانه‌های مشابه):
7
+ - نماد همیشه زنده روی search.codal.ir چک می‌شه -> نمادهای تازه (توسن) از روز اول کار می‌کنن.
8
+ - صورت‌هایِ مالیِ اصلی از متغیرِ جاواسکریپتِ `datasource` توی صفحه‌ی گزارش خونده می‌شن، نه از دانلودِ اکسل.
9
+ - چندتا sheet (شرکت‌هایِ فرعی/وابسته، اعضایِ هیئت‌مدیره) اصلاً تویِ اون
10
+ datasource نیستن -- برایِ اون‌ها ضمیمه‌ی اکسلِ خودِ اطلاعیه با تطابقِ
11
+ دقیقِ اسمِ شیت خونده می‌شه (fetch_excel_sheet؛ نگاه کن به CHANGELOG ۰.۸.۹).
12
+ - سرفصل‌ها با row_code ایندکس می‌شن (پایدارتر از تطبیقِ متنی).
13
+ - ردیف‌های توضیحیِ بی‌مقدار (زیرِ جدول) به‌صورتِ پیش‌فرض حذف می‌شن.
14
+ - قبل از گرفتنِ datasource، عنوانِ اطلاعیه چک می‌شه تا وقتِ اطلاعیه‌های غیرمالی هدر نره.
15
+ """
16
+
17
+ from ._version import __version__
18
+
19
+ BRAND = "Alilnvest"
20
+ DISTRIBUTION_NAME = "Alilnvest-Codal"
21
+ from .exceptions import CodalError, SymbolNotFoundError, DataSourceError, NetworkError, ExcelParseError, RateLimitError
22
+ from .http import reset_circuit_breaker, configure, clear_cache, safe_mode, fast_mode, symbol_pause, reset_symbol_pacing
23
+ from .letter_codes import LETTER_CODES, describe_letter_code
24
+ from .utils import validate_jdate
25
+ from .progress import ConsoleProgress, SearchProgress
26
+ from .models import Letter, DataSourceResult, FinancialReport, PortfolioReport, ReportQuality, SubsidiaryCompany, BoardMember, FinancialSnapshot
27
+ from .search import search_letters, get_letter, latest_letter
28
+ from .datasource import get_datasource, get_datasource_result, fetch_report_html
29
+ from .sheets import SheetOption, SheetContent, parse_sheet_menu, list_sheets, read_sheet
30
+ from .frames import cells_to_dataframe, cells_to_grid, apply_row_code_map
31
+ from .reports import (
32
+ fetch_financial_reports,
33
+ fetch_structured_sheet,
34
+ fetch_excel_sheet,
35
+ fetch_company_snapshot,
36
+ FULL_STATEMENT_SHEET_IDS,
37
+ income_statement,
38
+ balance_sheet,
39
+ monthly_activity,
40
+ cash_flow_statement,
41
+ equity_changes_statement,
42
+ comprehensive_income_statement,
43
+ subsidiary_companies,
44
+ affiliated_companies,
45
+ board_members,
46
+ latest_income,
47
+ latest_balance_sheet,
48
+ latest_cash_flow,
49
+ list_subsidiaries,
50
+ list_affiliates,
51
+ list_board_members,
52
+ financial_snapshot,
53
+ compare_report_data,
54
+ auditor_opinion_text,
55
+ fund_portfolio,
56
+ looks_like_financial_report,
57
+ looks_like_portfolio_report,
58
+ )
59
+
60
+ from .checks import CheckResult, analyze_statement, analyze_set, analyze_portfolio, statement_kind, cash_flow_method
61
+ from .audit import SymbolAudit, audit_symbol, audit_symbols, write_reports, detect_kind
62
+ __all__ = [
63
+ "CheckResult", "analyze_statement", "analyze_set", "analyze_portfolio",
64
+ "statement_kind", "cash_flow_method", "SymbolAudit", "audit_symbol", "audit_symbols",
65
+ "write_reports", "detect_kind",
66
+ "CodalError",
67
+ "SymbolNotFoundError",
68
+ "DataSourceError",
69
+ "NetworkError",
70
+ "ExcelParseError",
71
+ "RateLimitError",
72
+ "reset_circuit_breaker",
73
+ "configure",
74
+ "clear_cache",
75
+ "safe_mode",
76
+ "fast_mode",
77
+ "symbol_pause",
78
+ "reset_symbol_pacing",
79
+ "LETTER_CODES",
80
+ "describe_letter_code",
81
+ "validate_jdate",
82
+ "SearchProgress",
83
+ "ConsoleProgress",
84
+ "Letter",
85
+ "DataSourceResult",
86
+ "FinancialReport",
87
+ "PortfolioReport",
88
+ "ReportQuality",
89
+ "SubsidiaryCompany",
90
+ "BoardMember",
91
+ "FinancialSnapshot",
92
+ "search_letters",
93
+ "get_letter",
94
+ "latest_letter",
95
+ "get_datasource",
96
+ "get_datasource_result",
97
+ "fetch_report_html",
98
+ "SheetOption",
99
+ "SheetContent",
100
+ "parse_sheet_menu",
101
+ "list_sheets",
102
+ "read_sheet",
103
+ "cells_to_dataframe",
104
+ "cells_to_grid",
105
+ "apply_row_code_map",
106
+ "fetch_financial_reports",
107
+ "fetch_structured_sheet",
108
+ "fetch_excel_sheet",
109
+ "fetch_company_snapshot",
110
+ "FULL_STATEMENT_SHEET_IDS",
111
+ "income_statement",
112
+ "balance_sheet",
113
+ "monthly_activity",
114
+ "cash_flow_statement",
115
+ "equity_changes_statement",
116
+ "comprehensive_income_statement",
117
+ "subsidiary_companies",
118
+ "affiliated_companies",
119
+ "board_members",
120
+ "latest_income",
121
+ "latest_balance_sheet",
122
+ "latest_cash_flow",
123
+ "list_subsidiaries",
124
+ "list_affiliates",
125
+ "list_board_members",
126
+ "financial_snapshot",
127
+ "compare_report_data",
128
+ "auditor_opinion_text",
129
+ "fund_portfolio",
130
+ "looks_like_financial_report",
131
+ "looks_like_portfolio_report",
132
+ "__version__",
133
+ "BRAND",
134
+ "DISTRIBUTION_NAME",
135
+ ]
136
+
137
+
138
+ # 1.0 additions; the imports above intentionally remain for 0.9 compatibility.
139
+ from .api import (
140
+ async_balance_sheet,
141
+ async_income_statement,
142
+ balance_sheet as modern_balance_sheet,
143
+ income_statement as modern_income_statement,
144
+ )
145
+ from .cache import Cache
146
+ from .config import CodalConfig, DEFAULT_CONFIG
147
+ from .endpoints import BROWSER_HEADERS, CodalEndpoint
148
+ from .http import CodalHTTPClient
149
+ from .exceptions import CodalNetworkError, CodalNotFoundError, CodalParsingError
150
+ from .codal import Codal
151
+ from .health import health_check
152
+ from .automation import CodalRun, CodalBot, OperationResult, fetch_since
153
+ from .store import SQLiteStore, StoredDatasource
154
+
155
+ # New names are aliases at the package boundary; the legacy names above retain
156
+ # their richer optional arguments and historical report types.
157
+ __all__ += [
158
+ "CodalConfig", "DEFAULT_CONFIG", "Cache", "CodalEndpoint", "BROWSER_HEADERS",
159
+ "CodalHTTPClient", "CodalNetworkError", "CodalNotFoundError", "CodalParsingError",
160
+ "async_income_statement", "async_balance_sheet", "modern_income_statement", "modern_balance_sheet", "Codal", "health_check", "CodalRun", "CodalBot", "OperationResult", "fetch_since",
161
+ "SQLiteStore", "StoredDatasource",
162
+ ]
163
+ # The stable 1.0 API must win at the package boundary. The underlying legacy
164
+ # implementation is still available as reports.income_statement and retains
165
+ # its historical optional arguments.
166
+ income_statement = modern_income_statement # noqa: F811 — legacy name intentionally replaced at package boundary
167
+ balance_sheet = modern_balance_sheet # noqa: F811 — legacy name intentionally replaced at package boundary
@@ -0,0 +1,2 @@
1
+ """Package version."""
2
+ __version__ = "1.5.4"
@@ -0,0 +1,82 @@
1
+ """اصلاحیه: اطلاعیه‌ی اصلاح‌شده را با اولیه‌اش می‌سنجد و می‌گوید اصلاحیه کدام عددها را عوض کرده است.
2
+
3
+ کدال گاهی یک اطلاعیه را «اصلاحیه» می‌کند و هر دو نسخه جداگانه منتشر می‌شوند. این ماژول نسخه‌ی اولیه‌ی همان دوره
4
+ را پیدا می‌کند (همان پایان دوره، همان نوع تلفیقی یا غیرتلفیقی، و بدون عبارت «اصلاحیه») و عددهایی را که فرق دارند
5
+ فهرست می‌کند. این ماژول خالص است؛ جست‌وجوی شبکه در audit.py انجام می‌شود.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Iterable
11
+
12
+ from .checks import CheckResult, column_layout, fa_digits, index_rows, period_of, row_key
13
+
14
+ __all__ = ["AMENDMENT_MARK", "is_amendment", "find_original", "differences", "compare_with_original"]
15
+
16
+ AMENDMENT_MARK = "اصلاحیه"
17
+ TOLERANCE = 1.0 # ریال
18
+ KIND_NAMES = {"balance": "ترازنامه", "income": "سود و زیان", "cash_flow": "جریان نقد"}
19
+
20
+
21
+ def is_amendment(report: Any) -> bool:
22
+ return AMENDMENT_MARK in row_key(getattr(report, "title", "") or "")
23
+
24
+
25
+ def find_original(current: Any, candidates: Iterable[Any]) -> Any | None:
26
+ """اولین اطلاعیه‌ای که همان پایان دوره و همان نوع (تلفیقی/غیرتلفیقی) را دارد و اصلاحیه نیست."""
27
+ matches = []
28
+ for rep in candidates:
29
+ if rep is None or rep.tracing_no == current.tracing_no or is_amendment(rep):
30
+ continue
31
+ if rep.period_end_to_date != current.period_end_to_date:
32
+ continue
33
+ if bool(rep.is_consolidated) != bool(current.is_consolidated):
34
+ continue
35
+ matches.append(rep)
36
+ if not matches:
37
+ return None
38
+ def sort_key(rep):
39
+ date = getattr(rep, "publish_date_time", None) or getattr(rep, "sent_date_time", None) or "~"
40
+ return (date, int(rep.tracing_no) if rep.tracing_no is not None else 2**63 - 1)
41
+ return min(matches, key=sort_key)
42
+
43
+
44
+ def _values(report: Any) -> dict[tuple[str, str], tuple[str, float]]:
45
+ """(کلید برچسب، تاریخ ستون) -> (برچسب، مقدار). اولین رخداد هر کلید نگه داشته می‌شود."""
46
+ layout = column_layout(report.raw)
47
+ out: dict[tuple[str, str], tuple[str, float]] = {}
48
+ for row in index_rows(report.row_code_df):
49
+ if not row["key"]:
50
+ continue
51
+ for col, value in row["vals"].items():
52
+ if col <= 1:
53
+ continue
54
+ date = period_of(layout, col)
55
+ if date:
56
+ out.setdefault((row["key"], date), (row["label"], value))
57
+ return out
58
+
59
+
60
+ def differences(current: Any, original: Any, *, tolerance: float = TOLERANCE) -> list[dict[str, Any]]:
61
+ """عددهایی که در اصلاحیه (current) با اطلاعیه‌ی اولیه (original) فرق دارند."""
62
+ new, old = _values(current), _values(original)
63
+ diffs = []
64
+ for key in sorted(set(new) & set(old), key=lambda k: (k[1], k[0])):
65
+ label, before = old[key]
66
+ _, after = new[key]
67
+ if abs(before - after) > tolerance:
68
+ diffs.append({"label": label, "period": key[1], "original": before, "amended": after})
69
+ return diffs
70
+
71
+
72
+ def compare_with_original(kind: str, current: Any, original: Any) -> CheckResult:
73
+ name = f"{KIND_NAMES.get(kind, kind)}: اصلاحیه در برابر اطلاعیه‌ی اولیه"
74
+ head = f"TracingNo={current.tracing_no} (اصلاحیه) در برابر TracingNo={original.tracing_no} (اولیه)"
75
+ diffs = differences(current, original)
76
+ if not diffs:
77
+ return CheckResult("ok", "اصلاحیه", name, f"{head}: هیچ عددی تغییر نکرده است")
78
+ details = [f"{d['label']} | {fa_digits(d['period'])}: {d['original']:,.0f} → {d['amended']:,.0f}"
79
+ for d in diffs[:15]]
80
+ if len(diffs) > 15:
81
+ details.append(f"و {len(diffs) - 15} مورد دیگر")
82
+ return CheckResult("info", "اصلاحیه", name, f"{head}: {len(diffs)} عدد تغییر کرده است", tuple(details))
@@ -0,0 +1,174 @@
1
+ """پاک‌سازی، مدل‌سازی، export و مقایسه‌ی خروجی‌ها -- لایه‌یِ کمکیِ اختیاری، بالایِ خروجیِ خامِ reports.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from typing import Any
8
+
9
+ import pandas as pd
10
+
11
+ from .models import BoardMember, ReportQuality, SubsidiaryCompany
12
+ from .utils import normalize_fa
13
+
14
+ # نگاشتِ اسمِ ستون‌هایِ فارسیِ رایج (تویِ شیت‌هایِ «شرکت های فرعی»/«شرکت های
15
+ # وابسته») به اسمِ فیلدِ انگلیسیِ SubsidiaryCompany -- برایِ standardize_columns.
16
+ SUBSIDIARY_COLUMN_ALIASES = {
17
+ "نماد شرکت": "symbol",
18
+ "نام شرکت": "name",
19
+ "نماد ثبت شده نزد سازمان": "registered_symbol",
20
+ "وضعیت ثبت شده نزد سازمان": "registration_status",
21
+ "نوع فعالیت": "activity_type",
22
+ "حسابرس": "auditor",
23
+ "سال مالی": "fiscal_year",
24
+ "فعال/غیر فعال": "active_status",
25
+ "وضعیت": "status",
26
+ }
27
+
28
+
29
+ def _unique_columns(columns: list[Any]) -> list[str]:
30
+ """نام ستون‌ها را پایدار و یکتا می‌کند تا ستون تکراری بی‌صدا ادغام نشود."""
31
+ seen: dict[str, int] = {}
32
+ result: list[str] = []
33
+ for value in columns:
34
+ base = str(value).strip() or "column"
35
+ count = seen.get(base, 0)
36
+ seen[base] = count + 1
37
+ result.append(base if count == 0 else f"{base}_{count + 1}")
38
+ return result
39
+
40
+
41
+ def clean_table(df: pd.DataFrame, *, drop_header: bool = True) -> tuple[pd.DataFrame, ReportQuality]:
42
+ """
43
+ خروجیِ خامِ grid_df (که address-based ـه، نه row_code) رو تمیز می‌کنه:
44
+ ستون‌هایِ کاملاً خالی و ردیف‌هایِ کاملاً تکراری رو حذف می‌کنه، و اگه
45
+ drop_header=True بود و ردیفِ اول با حداقل ۲ تا از اسم‌هایِ شناخته‌شده‌ی
46
+ SUBSIDIARY_COLUMN_ALIASES مطابقت داشت، همون ردیف رو به‌عنوانِ هدرِ
47
+ واقعی برمی‌داره (نه ستون‌هایِ عددیِ پیش‌فرضِ 0/1/2/...).
48
+
49
+ خروجیِ دوم (ReportQuality) شفاف می‌گه چقدر ردیف/ستون حذف شد -- تا کاربر
50
+ کورکورانه به تمیزکاری اعتماد نکنه.
51
+ """
52
+ out = df.copy()
53
+ original_column_count = len(out.columns)
54
+ duplicate_rows = int(out.duplicated().sum())
55
+
56
+ out = out.dropna(axis=1, how="all").drop_duplicates().reset_index(drop=True)
57
+
58
+ if drop_header and not out.empty:
59
+ first_row_normalized = [normalize_fa(str(x)) for x in out.iloc[0].tolist()]
60
+ known_headers_normalized = {normalize_fa(k) for k in SUBSIDIARY_COLUMN_ALIASES}
61
+ if len(set(first_row_normalized) & known_headers_normalized) >= 2:
62
+ out.columns = _unique_columns(list(out.iloc[0]))
63
+ out = out.iloc[1:].reset_index(drop=True)
64
+
65
+ quality = ReportQuality(
66
+ "cleaned_table",
67
+ duplicate_rows_removed=duplicate_rows,
68
+ empty_columns_removed=original_column_count - len(out.columns),
69
+ )
70
+ return out, quality
71
+
72
+
73
+ def standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
74
+ """ستون‌هایی که اسمِ فارسیِ شناخته‌شده دارن (SUBSIDIARY_COLUMN_ALIASES) رو به اسمِ فیلدِ انگلیسی تغییر می‌ده؛ بقیه دست‌نخورده می‌مونن."""
75
+ out = df.copy()
76
+ aliases_normalized = {normalize_fa(k): v for k, v in SUBSIDIARY_COLUMN_ALIASES.items()}
77
+ out.columns = _unique_columns([aliases_normalized.get(normalize_fa(str(c)), str(c)) for c in out.columns])
78
+ return out
79
+
80
+
81
+ def subsidiary_models(df: pd.DataFrame) -> list[SubsidiaryCompany]:
82
+ """grid_dfِ تمیزشده‌یِ شرکت‌هایِ فرعی/وابسته رو به لیستی از SubsidiaryCompany تبدیل می‌کنه."""
83
+ out = standardize_columns(df)
84
+ allowed_fields = set(SubsidiaryCompany.__dataclass_fields__) - {"raw"}
85
+ result: list[SubsidiaryCompany] = []
86
+ for _, row in out.iterrows():
87
+ values = {
88
+ field_name: (None if field_name not in row or pd.isna(row[field_name]) else str(row[field_name]))
89
+ for field_name in allowed_fields
90
+ }
91
+ result.append(SubsidiaryCompany(**values, raw=row.to_dict()))
92
+ return result
93
+
94
+
95
+ def board_models(df: pd.DataFrame) -> list[BoardMember]:
96
+ """
97
+ grid_dfِ تمیزشده‌یِ اعضایِ هیئت‌مدیره رو به لیستی از BoardMember تبدیل
98
+ می‌کنه. اگه ستونِ "name"/"role" (بعدِ standardize_columns) پیدا نشد،
99
+ به‌ترتیب اولین/دومین ستون رو به‌عنوانِ fallback به‌کار می‌بره -- چون
100
+ اسمِ دقیقِ ستون‌هایِ این شیت هنوز تویِ SUBSIDIARY_COLUMN_ALIASES نیستن.
101
+ """
102
+ out = standardize_columns(df)
103
+ columns = list(out.columns)
104
+ name_column = "name" if "name" in columns else (columns[0] if columns else None)
105
+ role_column = "role" if "role" in columns else (columns[1] if len(columns) > 1 else None)
106
+
107
+ result: list[BoardMember] = []
108
+ for _, row in out.iterrows():
109
+ name = None if name_column is None or pd.isna(row.get(name_column)) else str(row.get(name_column))
110
+ role = None if role_column is None or pd.isna(row.get(role_column)) else str(row.get(role_column))
111
+ result.append(BoardMember(name, role, row.to_dict()))
112
+ return result
113
+
114
+
115
+ def dataframe_to_json(df: pd.DataFrame, path: str, *, metadata: dict | None = None, **kwargs: Any) -> None:
116
+ """DataFrame رو به یه فایلِ JSON با ساختارِ {"metadata": ..., "rows": [...]} می‌نویسه (UTF-8، بدونِ escapeِ فارسی)."""
117
+ def json_safe(value: Any) -> Any:
118
+ if value is None or isinstance(value, (str, bool, int)):
119
+ return value
120
+ if isinstance(value, float):
121
+ return value if math.isfinite(value) else None
122
+ if isinstance(value, dict):
123
+ return {str(k): json_safe(v) for k, v in value.items()}
124
+ if isinstance(value, (list, tuple)):
125
+ return [json_safe(v) for v in value]
126
+ try:
127
+ if pd.isna(value):
128
+ return None
129
+ except (TypeError, ValueError):
130
+ pass
131
+ return str(value)
132
+
133
+ rows = df.where(pd.notna(df), None).to_dict(orient="records")
134
+ payload = {"metadata": json_safe(metadata or {}), "rows": json_safe(rows)}
135
+ kwargs.setdefault("ensure_ascii", False)
136
+ kwargs.setdefault("indent", 2)
137
+ with open(path, "w", encoding="utf-8") as f:
138
+ json.dump(payload, f, **kwargs)
139
+
140
+
141
+ def compare_reports(old: Any, new: Any, *, key: str | None = None) -> dict[str, Any]:
142
+ """
143
+ دو گزارش (یا مستقیم دو DataFrame) رو مقایسه می‌کنه -- `old`/`new` می‌تونن
144
+ خودِ FinancialReport/PortfolioReport باشن (grid_df/row_code_df ازشون
145
+ خونده می‌شه) یا مستقیم DataFrame.
146
+
147
+ اگه key بدی و تویِ هر دو ستون باشه، بر اساسِ اون ایندکس می‌کنه (مقایسه‌ی
148
+ دقیق‌تر بر اساسِ شناسه، نه شماره‌ی ردیف). خروجی: کدوم ردیف‌ها اضافه
149
+ شدن، کدوم حذف شدن، کدوم مقدارشون فرق کرده.
150
+ """
151
+ old_df = getattr(old, "grid_df", getattr(old, "row_code_df", old)).copy()
152
+ new_df = getattr(new, "grid_df", getattr(new, "row_code_df", new)).copy()
153
+
154
+ if key and key in old_df.columns and key in new_df.columns:
155
+ old_df, new_df = old_df.set_index(key), new_df.set_index(key)
156
+ if not (old_df.index.is_unique and new_df.index.is_unique):
157
+ # با ایندکسِ تکراری، loc یک DataFrame برمی‌گرداند نه یک سطر؛ مقایسه بی‌معنی می‌شود.
158
+ raise ValueError("compare_reports: شناسه‌ها باید یکتا باشند (ردیفِ تکراری در یکی از گزارش‌ها).")
159
+
160
+ old_keys, new_keys = set(old_df.index), set(new_df.index)
161
+
162
+ changed: list[dict[str, Any]] = []
163
+ for idx in sorted(old_keys & new_keys, key=str): # ترتیبِ قطعی برای خروجیِ قابل‌مقایسه
164
+ if not old_df.loc[idx].equals(new_df.loc[idx]):
165
+ changed.append({"key": idx, "old": old_df.loc[idx].to_dict(), "new": new_df.loc[idx].to_dict()})
166
+
167
+ added_keys = sorted(new_keys - old_keys, key=str)
168
+ removed_keys = sorted(old_keys - new_keys, key=str)
169
+ return {
170
+ # reset_index تا کلیدِ ردیف (همان «key» در changed) در خروجی بماند؛ با to_dict("records") خالی گم می‌شد
171
+ "added": new_df.loc[added_keys].rename_axis("key").reset_index().to_dict("records"),
172
+ "removed": old_df.loc[removed_keys].rename_axis("key").reset_index().to_dict("records"),
173
+ "changed": changed,
174
+ }
alilnvest_codal/api.py ADDED
@@ -0,0 +1,69 @@
1
+ """Stable 1.0 public API.
2
+
3
+ The legacy report engine remains underneath so existing advanced features keep
4
+ working while callers receive keyword-only, injectable entry points.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from typing import Any
10
+
11
+ from .config import CodalConfig
12
+ from .http import CodalHTTPClient
13
+ from .reports import balance_sheet as _balance_sheet
14
+ from .reports import income_statement as _income_statement
15
+
16
+
17
+ def income_statement(
18
+ symbol: str,
19
+ *,
20
+ from_date: str | None = None,
21
+ max_reports: int | None = None,
22
+ use_cache: bool = True,
23
+ client: CodalHTTPClient | None = None,
24
+ **kwargs: Any,
25
+ ) -> list[Any]:
26
+ """Return income statements, preserving the mature Codal parser behavior."""
27
+ options = dict(kwargs)
28
+ if from_date is not None:
29
+ options["from_date"] = from_date
30
+ if max_reports is not None:
31
+ options["max_reports"] = max_reports
32
+ options["use_cache"] = use_cache
33
+ options["client"] = client
34
+ options.setdefault("max_concurrency", 5)
35
+ return _income_statement(symbol, **options)
36
+
37
+
38
+ def balance_sheet(
39
+ symbol: str,
40
+ *,
41
+ from_date: str | None = None,
42
+ max_reports: int | None = None,
43
+ use_cache: bool = True,
44
+ client: CodalHTTPClient | None = None,
45
+ **kwargs: Any,
46
+ ) -> list[Any]:
47
+ """Return balance sheets with the same compatibility guarantees as 0.9."""
48
+ options = dict(kwargs)
49
+ if from_date is not None:
50
+ options["from_date"] = from_date
51
+ if max_reports is not None:
52
+ options["max_reports"] = max_reports
53
+ options["use_cache"] = use_cache
54
+ options["client"] = client
55
+ options.setdefault("max_concurrency", 5)
56
+ return _balance_sheet(symbol, **options)
57
+
58
+
59
+ async def async_income_statement(symbol: str, **kwargs: Any) -> list[Any]:
60
+ """Async facade for applications that need a non-blocking call site."""
61
+ return await asyncio.to_thread(income_statement, symbol, **kwargs)
62
+
63
+
64
+ async def async_balance_sheet(symbol: str, **kwargs: Any) -> list[Any]:
65
+ """Async facade for applications that need a non-blocking call site."""
66
+ return await asyncio.to_thread(balance_sheet, symbol, **kwargs)
67
+
68
+
69
+ __all__ = ["income_statement", "balance_sheet", "async_income_statement", "async_balance_sheet", "CodalConfig"]