borsapy 0.4.0__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.
- borsapy/__init__.py +134 -0
- borsapy/_models/__init__.py +1 -0
- borsapy/_providers/__init__.py +5 -0
- borsapy/_providers/base.py +94 -0
- borsapy/_providers/bist_index.py +150 -0
- borsapy/_providers/btcturk.py +230 -0
- borsapy/_providers/canlidoviz.py +773 -0
- borsapy/_providers/dovizcom.py +869 -0
- borsapy/_providers/dovizcom_calendar.py +276 -0
- borsapy/_providers/dovizcom_tahvil.py +172 -0
- borsapy/_providers/hedeffiyat.py +376 -0
- borsapy/_providers/isin.py +247 -0
- borsapy/_providers/isyatirim.py +943 -0
- borsapy/_providers/isyatirim_screener.py +468 -0
- borsapy/_providers/kap.py +534 -0
- borsapy/_providers/paratic.py +278 -0
- borsapy/_providers/tcmb.py +317 -0
- borsapy/_providers/tefas.py +802 -0
- borsapy/_providers/viop.py +204 -0
- borsapy/bond.py +162 -0
- borsapy/cache.py +86 -0
- borsapy/calendar.py +272 -0
- borsapy/crypto.py +153 -0
- borsapy/exceptions.py +64 -0
- borsapy/fund.py +471 -0
- borsapy/fx.py +388 -0
- borsapy/index.py +285 -0
- borsapy/inflation.py +166 -0
- borsapy/market.py +53 -0
- borsapy/multi.py +227 -0
- borsapy/screener.py +365 -0
- borsapy/ticker.py +1196 -0
- borsapy/viop.py +162 -0
- borsapy-0.4.0.dist-info/METADATA +969 -0
- borsapy-0.4.0.dist-info/RECORD +37 -0
- borsapy-0.4.0.dist-info/WHEEL +4 -0
- borsapy-0.4.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""VİOP provider for derivatives data via İş Yatırım HTML scraping."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
|
|
6
|
+
from borsapy._providers.base import BaseProvider
|
|
7
|
+
from borsapy.cache import TTL
|
|
8
|
+
from borsapy.exceptions import APIError
|
|
9
|
+
|
|
10
|
+
# Singleton instance
|
|
11
|
+
_viop_provider: "ViOpProvider | None" = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_viop_provider() -> "ViOpProvider":
|
|
15
|
+
"""Get singleton VİOP provider instance."""
|
|
16
|
+
global _viop_provider
|
|
17
|
+
if _viop_provider is None:
|
|
18
|
+
_viop_provider = ViOpProvider()
|
|
19
|
+
return _viop_provider
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ViOpProvider(BaseProvider):
|
|
23
|
+
"""
|
|
24
|
+
Provider for VİOP (Vadeli İşlem ve Opsiyon Piyasası) data.
|
|
25
|
+
|
|
26
|
+
Data source: İş Yatırım VİOP page (HTML scraping)
|
|
27
|
+
Note: Data is delayed by ~15 minutes (Matriks source)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
URL = "https://www.isyatirim.com.tr/tr-tr/analiz/Sayfalar/viop.aspx"
|
|
31
|
+
|
|
32
|
+
# Table section identifiers (Turkish)
|
|
33
|
+
SECTIONS = {
|
|
34
|
+
"stock_futures": "Pay Vadeli İşlem Ana Pazarı",
|
|
35
|
+
"index_futures": "Endeks Vadeli İşlem Ana Pazarı",
|
|
36
|
+
"currency_futures": "Döviz Vadeli İşlem Ana Pazarı",
|
|
37
|
+
"commodity_futures": "Kıymetli Madenler Vadeli İşlem Ana Pazarı",
|
|
38
|
+
"stock_options": "Pay Opsiyon Ana Pazarı",
|
|
39
|
+
"index_options": "Endeks Opsiyon Ana Pazarı",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def _fetch_page(self) -> BeautifulSoup:
|
|
43
|
+
"""Fetch and parse VİOP page."""
|
|
44
|
+
cache_key = "viop:page"
|
|
45
|
+
cached = self._cache_get(cache_key)
|
|
46
|
+
if cached is not None:
|
|
47
|
+
return cached
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
response = self._get(self.URL)
|
|
51
|
+
soup = BeautifulSoup(response.text, "html.parser")
|
|
52
|
+
self._cache_set(cache_key, soup, TTL.VIOP)
|
|
53
|
+
return soup
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise APIError(f"Failed to fetch VİOP page: {e}") from e
|
|
56
|
+
|
|
57
|
+
def _parse_table(self, soup: BeautifulSoup, section_name: str) -> pd.DataFrame:
|
|
58
|
+
"""Parse a VİOP table section."""
|
|
59
|
+
# Find accordion with the section name
|
|
60
|
+
accordion = None
|
|
61
|
+
for a_tag in soup.find_all("a"):
|
|
62
|
+
if a_tag.get_text(strip=True) == section_name:
|
|
63
|
+
accordion = a_tag.find_parent("div", class_="accordion-item")
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if accordion is None:
|
|
67
|
+
return pd.DataFrame()
|
|
68
|
+
|
|
69
|
+
# Find table within accordion
|
|
70
|
+
table = accordion.find("table")
|
|
71
|
+
if table is None:
|
|
72
|
+
return pd.DataFrame()
|
|
73
|
+
|
|
74
|
+
rows = []
|
|
75
|
+
for tr in table.find_all("tr"):
|
|
76
|
+
tds = tr.find_all("td")
|
|
77
|
+
if len(tds) >= 5:
|
|
78
|
+
# Extract contract code from title attribute
|
|
79
|
+
first_td = tds[0]
|
|
80
|
+
title = first_td.get("title", "")
|
|
81
|
+
contract_code = ""
|
|
82
|
+
if "|" in title:
|
|
83
|
+
contract_code = title.split("|")[0].strip()
|
|
84
|
+
|
|
85
|
+
contract_name = first_td.get_text(strip=True)
|
|
86
|
+
price = self._parse_number(tds[1].get_text(strip=True))
|
|
87
|
+
change = self._parse_number(tds[2].get_text(strip=True))
|
|
88
|
+
volume_tl = self._parse_number(tds[3].get_text(strip=True))
|
|
89
|
+
volume_qty = self._parse_number(tds[4].get_text(strip=True))
|
|
90
|
+
|
|
91
|
+
rows.append({
|
|
92
|
+
"code": contract_code,
|
|
93
|
+
"contract": contract_name,
|
|
94
|
+
"price": price,
|
|
95
|
+
"change": change,
|
|
96
|
+
"volume_tl": volume_tl,
|
|
97
|
+
"volume_qty": volume_qty,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if not rows:
|
|
101
|
+
return pd.DataFrame()
|
|
102
|
+
|
|
103
|
+
return pd.DataFrame(rows)
|
|
104
|
+
|
|
105
|
+
def _parse_number(self, text: str) -> float | None:
|
|
106
|
+
"""Parse Turkish number format (1.234,56 -> 1234.56)."""
|
|
107
|
+
if not text:
|
|
108
|
+
return None
|
|
109
|
+
try:
|
|
110
|
+
# Remove thousand separator (.) and convert decimal separator (,) to (.)
|
|
111
|
+
cleaned = text.replace(".", "").replace(",", ".")
|
|
112
|
+
return float(cleaned)
|
|
113
|
+
except (ValueError, TypeError):
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def get_futures(self, category: str = "all") -> pd.DataFrame:
|
|
117
|
+
"""
|
|
118
|
+
Get futures contracts.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
category: Filter by category:
|
|
122
|
+
- "all": All futures
|
|
123
|
+
- "stock": Stock futures (Pay Vadeli)
|
|
124
|
+
- "index": Index futures (Endeks Vadeli)
|
|
125
|
+
- "currency": Currency futures (Döviz Vadeli)
|
|
126
|
+
- "commodity": Commodity futures (Kıymetli Madenler)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
DataFrame with columns: code, contract, price, change, volume_tl, volume_qty
|
|
130
|
+
"""
|
|
131
|
+
soup = self._fetch_page()
|
|
132
|
+
|
|
133
|
+
category_map = {
|
|
134
|
+
"stock": ["stock_futures"],
|
|
135
|
+
"index": ["index_futures"],
|
|
136
|
+
"currency": ["currency_futures"],
|
|
137
|
+
"commodity": ["commodity_futures"],
|
|
138
|
+
"all": ["stock_futures", "index_futures", "currency_futures", "commodity_futures"],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
sections = category_map.get(category, category_map["all"])
|
|
142
|
+
dfs = []
|
|
143
|
+
|
|
144
|
+
for section_key in sections:
|
|
145
|
+
section_name = self.SECTIONS.get(section_key)
|
|
146
|
+
if section_name:
|
|
147
|
+
df = self._parse_table(soup, section_name)
|
|
148
|
+
if not df.empty:
|
|
149
|
+
df["category"] = section_key.replace("_futures", "")
|
|
150
|
+
dfs.append(df)
|
|
151
|
+
|
|
152
|
+
if not dfs:
|
|
153
|
+
return pd.DataFrame(columns=["code", "contract", "price", "change", "volume_tl", "volume_qty", "category"])
|
|
154
|
+
|
|
155
|
+
return pd.concat(dfs, ignore_index=True)
|
|
156
|
+
|
|
157
|
+
def get_options(self, category: str = "all") -> pd.DataFrame:
|
|
158
|
+
"""
|
|
159
|
+
Get options contracts.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
category: Filter by category:
|
|
163
|
+
- "all": All options
|
|
164
|
+
- "stock": Stock options (Pay Opsiyon)
|
|
165
|
+
- "index": Index options (Endeks Opsiyon)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
DataFrame with columns: code, contract, price, change, volume_tl, volume_qty
|
|
169
|
+
"""
|
|
170
|
+
soup = self._fetch_page()
|
|
171
|
+
|
|
172
|
+
category_map = {
|
|
173
|
+
"stock": ["stock_options"],
|
|
174
|
+
"index": ["index_options"],
|
|
175
|
+
"all": ["stock_options", "index_options"],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
sections = category_map.get(category, category_map["all"])
|
|
179
|
+
dfs = []
|
|
180
|
+
|
|
181
|
+
for section_key in sections:
|
|
182
|
+
section_name = self.SECTIONS.get(section_key)
|
|
183
|
+
if section_name:
|
|
184
|
+
df = self._parse_table(soup, section_name)
|
|
185
|
+
if not df.empty:
|
|
186
|
+
df["category"] = section_key.replace("_options", "")
|
|
187
|
+
dfs.append(df)
|
|
188
|
+
|
|
189
|
+
if not dfs:
|
|
190
|
+
return pd.DataFrame(columns=["code", "contract", "price", "change", "volume_tl", "volume_qty", "category"])
|
|
191
|
+
|
|
192
|
+
return pd.concat(dfs, ignore_index=True)
|
|
193
|
+
|
|
194
|
+
def get_all(self) -> dict[str, pd.DataFrame]:
|
|
195
|
+
"""
|
|
196
|
+
Get all VİOP data.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dictionary with 'futures' and 'options' DataFrames.
|
|
200
|
+
"""
|
|
201
|
+
return {
|
|
202
|
+
"futures": self.get_futures("all"),
|
|
203
|
+
"options": self.get_options("all"),
|
|
204
|
+
}
|
borsapy/bond.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Bond class for Turkish government bond yields - yfinance-like API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from borsapy._providers.dovizcom_tahvil import get_tahvil_provider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Bond:
|
|
11
|
+
"""
|
|
12
|
+
A yfinance-like interface for Turkish government bond data.
|
|
13
|
+
|
|
14
|
+
Data source: doviz.com/tahvil
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
>>> import borsapy as bp
|
|
18
|
+
>>> bond = bp.Bond("10Y")
|
|
19
|
+
>>> bond.yield_rate # Current yield (e.g., 28.03)
|
|
20
|
+
28.03
|
|
21
|
+
>>> bond.yield_decimal # As decimal (e.g., 0.2803)
|
|
22
|
+
0.2803
|
|
23
|
+
>>> bond.change_pct # Daily change percentage
|
|
24
|
+
1.5
|
|
25
|
+
|
|
26
|
+
>>> bp.bonds() # Get all bond yields
|
|
27
|
+
name maturity yield change change_pct
|
|
28
|
+
0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54
|
|
29
|
+
1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31
|
|
30
|
+
2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Valid maturities
|
|
34
|
+
MATURITIES = ["2Y", "5Y", "10Y"]
|
|
35
|
+
|
|
36
|
+
def __init__(self, maturity: str):
|
|
37
|
+
"""
|
|
38
|
+
Initialize a Bond object.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
maturity: Bond maturity (2Y, 5Y, 10Y).
|
|
42
|
+
"""
|
|
43
|
+
self._maturity = maturity.upper()
|
|
44
|
+
self._provider = get_tahvil_provider()
|
|
45
|
+
self._data_cache: dict[str, Any] | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def maturity(self) -> str:
|
|
49
|
+
"""Return the bond maturity."""
|
|
50
|
+
return self._maturity
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def _data(self) -> dict[str, Any]:
|
|
54
|
+
"""Get bond data (cached)."""
|
|
55
|
+
if self._data_cache is None:
|
|
56
|
+
self._data_cache = self._provider.get_bond(self._maturity)
|
|
57
|
+
return self._data_cache
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def name(self) -> str:
|
|
61
|
+
"""Return the bond name."""
|
|
62
|
+
return self._data.get("name", "")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def yield_rate(self) -> float | None:
|
|
66
|
+
"""
|
|
67
|
+
Return the current yield as percentage.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Yield rate as percentage (e.g., 28.03 for 28.03%).
|
|
71
|
+
"""
|
|
72
|
+
return self._data.get("yield")
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def yield_decimal(self) -> float | None:
|
|
76
|
+
"""
|
|
77
|
+
Return the current yield as decimal.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Yield rate as decimal (e.g., 0.2803 for 28.03%).
|
|
81
|
+
Useful for financial calculations.
|
|
82
|
+
"""
|
|
83
|
+
return self._data.get("yield_decimal")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def change(self) -> float | None:
|
|
87
|
+
"""Return the absolute change in yield."""
|
|
88
|
+
return self._data.get("change")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def change_pct(self) -> float | None:
|
|
92
|
+
"""Return the percentage change in yield."""
|
|
93
|
+
return self._data.get("change_pct")
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def info(self) -> dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
Return all bond information.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dictionary with name, maturity, yield, change, etc.
|
|
102
|
+
"""
|
|
103
|
+
return self._data.copy()
|
|
104
|
+
|
|
105
|
+
def __repr__(self) -> str:
|
|
106
|
+
return f"Bond('{self._maturity}')"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def bonds() -> pd.DataFrame:
|
|
110
|
+
"""
|
|
111
|
+
Get all Turkish government bond yields.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
DataFrame with columns: name, maturity, yield, change, change_pct.
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
>>> import borsapy as bp
|
|
118
|
+
>>> bp.bonds()
|
|
119
|
+
name maturity yield change change_pct
|
|
120
|
+
0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54
|
|
121
|
+
1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31
|
|
122
|
+
2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52
|
|
123
|
+
"""
|
|
124
|
+
provider = get_tahvil_provider()
|
|
125
|
+
data = provider.get_bond_yields()
|
|
126
|
+
|
|
127
|
+
if not data:
|
|
128
|
+
return pd.DataFrame(columns=["name", "maturity", "yield", "change", "change_pct"])
|
|
129
|
+
|
|
130
|
+
df = pd.DataFrame(data)
|
|
131
|
+
|
|
132
|
+
# Select and rename columns
|
|
133
|
+
columns = {
|
|
134
|
+
"name": "name",
|
|
135
|
+
"maturity": "maturity",
|
|
136
|
+
"yield": "yield",
|
|
137
|
+
"change": "change",
|
|
138
|
+
"change_pct": "change_pct",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
df = df[[c for c in columns.keys() if c in df.columns]]
|
|
142
|
+
df = df.rename(columns=columns)
|
|
143
|
+
|
|
144
|
+
return df
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def risk_free_rate() -> float | None:
|
|
148
|
+
"""
|
|
149
|
+
Get the risk-free rate for Turkish market (10Y bond yield).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
10-year government bond yield as decimal.
|
|
153
|
+
Useful for CAPM and DCF calculations.
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
>>> import borsapy as bp
|
|
157
|
+
>>> rfr = bp.risk_free_rate()
|
|
158
|
+
>>> rfr
|
|
159
|
+
0.2803
|
|
160
|
+
"""
|
|
161
|
+
provider = get_tahvil_provider()
|
|
162
|
+
return provider.get_10y_yield()
|
borsapy/cache.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""TTL-based in-memory cache for borsapy."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CacheEntry(Generic[T]):
|
|
13
|
+
"""A single cache entry with value and expiration time."""
|
|
14
|
+
|
|
15
|
+
value: T
|
|
16
|
+
expires_at: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Cache:
|
|
21
|
+
"""Thread-safe TTL-based in-memory cache."""
|
|
22
|
+
|
|
23
|
+
_store: dict[str, CacheEntry] = field(default_factory=dict)
|
|
24
|
+
_lock: Lock = field(default_factory=Lock)
|
|
25
|
+
|
|
26
|
+
def get(self, key: str) -> Any | None:
|
|
27
|
+
"""Get a value from cache if it exists and hasn't expired."""
|
|
28
|
+
with self._lock:
|
|
29
|
+
entry = self._store.get(key)
|
|
30
|
+
if entry is None:
|
|
31
|
+
return None
|
|
32
|
+
if time.time() > entry.expires_at:
|
|
33
|
+
del self._store[key]
|
|
34
|
+
return None
|
|
35
|
+
return entry.value
|
|
36
|
+
|
|
37
|
+
def set(self, key: str, value: Any, ttl_seconds: int) -> None:
|
|
38
|
+
"""Set a value in cache with TTL in seconds."""
|
|
39
|
+
with self._lock:
|
|
40
|
+
self._store[key] = CacheEntry(value=value, expires_at=time.time() + ttl_seconds)
|
|
41
|
+
|
|
42
|
+
def delete(self, key: str) -> bool:
|
|
43
|
+
"""Delete a key from cache. Returns True if key existed."""
|
|
44
|
+
with self._lock:
|
|
45
|
+
if key in self._store:
|
|
46
|
+
del self._store[key]
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def clear(self) -> None:
|
|
51
|
+
"""Clear all entries from cache."""
|
|
52
|
+
with self._lock:
|
|
53
|
+
self._store.clear()
|
|
54
|
+
|
|
55
|
+
def cleanup(self) -> int:
|
|
56
|
+
"""Remove expired entries. Returns number of entries removed."""
|
|
57
|
+
with self._lock:
|
|
58
|
+
now = time.time()
|
|
59
|
+
expired_keys = [k for k, v in self._store.items() if now > v.expires_at]
|
|
60
|
+
for key in expired_keys:
|
|
61
|
+
del self._store[key]
|
|
62
|
+
return len(expired_keys)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# TTL values in seconds
|
|
66
|
+
class TTL:
|
|
67
|
+
"""Standard TTL values for different data types."""
|
|
68
|
+
|
|
69
|
+
REALTIME_PRICE = 60 # 1 minute
|
|
70
|
+
OHLCV_HISTORY = 3600 # 1 hour
|
|
71
|
+
COMPANY_INFO = 3600 # 1 hour
|
|
72
|
+
FINANCIAL_STATEMENTS = 86400 # 24 hours
|
|
73
|
+
FX_RATES = 300 # 5 minutes
|
|
74
|
+
COMPANY_LIST = 86400 # 24 hours
|
|
75
|
+
FUND_DATA = 3600 # 1 hour
|
|
76
|
+
INFLATION_DATA = 86400 # 24 hours
|
|
77
|
+
VIOP = 300 # 5 minutes (delayed data)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Global cache instance
|
|
81
|
+
_cache = Cache()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_cache() -> Cache:
|
|
85
|
+
"""Get the global cache instance."""
|
|
86
|
+
return _cache
|