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
borsapy/inflation.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Inflation class for TCMB inflation data - yfinance-like API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from borsapy._providers.tcmb import get_tcmb_provider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Inflation:
|
|
11
|
+
"""
|
|
12
|
+
A yfinance-like interface for Turkish inflation data from TCMB.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
>>> import borsapy as bp
|
|
16
|
+
>>> inf = bp.Inflation()
|
|
17
|
+
|
|
18
|
+
# Get latest inflation
|
|
19
|
+
>>> inf.latest()
|
|
20
|
+
{'date': '2024-11-01', 'yearly_inflation': 47.09, 'monthly_inflation': 2.24, ...}
|
|
21
|
+
|
|
22
|
+
# Get TÜFE history
|
|
23
|
+
>>> inf.tufe(limit=12) # Last 12 months
|
|
24
|
+
YearMonth YearlyInflation MonthlyInflation
|
|
25
|
+
Date
|
|
26
|
+
2024-11-01 11-2024 47.09 2.24
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
# Calculate inflation
|
|
30
|
+
>>> inf.calculate(100000, "2020-01", "2024-01")
|
|
31
|
+
{'initial_value': 100000, 'final_value': 342515.0, 'total_change': 242.52, ...}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""Initialize an Inflation object."""
|
|
36
|
+
self._provider = get_tcmb_provider()
|
|
37
|
+
|
|
38
|
+
def latest(self, inflation_type: str = "tufe") -> dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Get the latest inflation data.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
inflation_type: 'tufe' (CPI) or 'ufe' (PPI)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary with latest inflation data:
|
|
47
|
+
- date: Date string (YYYY-MM-DD)
|
|
48
|
+
- year_month: Month-Year string
|
|
49
|
+
- yearly_inflation: Year-over-year inflation rate
|
|
50
|
+
- monthly_inflation: Month-over-month inflation rate
|
|
51
|
+
- type: Inflation type (TUFE or UFE)
|
|
52
|
+
"""
|
|
53
|
+
return self._provider.get_latest(inflation_type)
|
|
54
|
+
|
|
55
|
+
def tufe(
|
|
56
|
+
self,
|
|
57
|
+
start: str | None = None,
|
|
58
|
+
end: str | None = None,
|
|
59
|
+
limit: int | None = None,
|
|
60
|
+
) -> pd.DataFrame:
|
|
61
|
+
"""
|
|
62
|
+
Get TÜFE (Consumer Price Index) data.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
start: Start date in YYYY-MM-DD format
|
|
66
|
+
end: End date in YYYY-MM-DD format
|
|
67
|
+
limit: Maximum number of records
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation.
|
|
71
|
+
Index is the Date.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> inf = Inflation()
|
|
75
|
+
>>> inf.tufe(limit=6) # Last 6 months
|
|
76
|
+
>>> inf.tufe(start="2023-01-01", end="2023-12-31") # 2023 data
|
|
77
|
+
"""
|
|
78
|
+
return self._provider.get_data("tufe", start, end, limit)
|
|
79
|
+
|
|
80
|
+
def ufe(
|
|
81
|
+
self,
|
|
82
|
+
start: str | None = None,
|
|
83
|
+
end: str | None = None,
|
|
84
|
+
limit: int | None = None,
|
|
85
|
+
) -> pd.DataFrame:
|
|
86
|
+
"""
|
|
87
|
+
Get ÜFE (Producer Price Index) data.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
start: Start date in YYYY-MM-DD format
|
|
91
|
+
end: End date in YYYY-MM-DD format
|
|
92
|
+
limit: Maximum number of records
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation.
|
|
96
|
+
Index is the Date.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> inf = Inflation()
|
|
100
|
+
>>> inf.ufe(limit=6) # Last 6 months
|
|
101
|
+
>>> inf.ufe(start="2023-01-01", end="2023-12-31") # 2023 data
|
|
102
|
+
"""
|
|
103
|
+
return self._provider.get_data("ufe", start, end, limit)
|
|
104
|
+
|
|
105
|
+
def calculate(
|
|
106
|
+
self,
|
|
107
|
+
amount: float,
|
|
108
|
+
start: str,
|
|
109
|
+
end: str,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Calculate inflation-adjusted value between two dates.
|
|
113
|
+
|
|
114
|
+
Uses TCMB's official inflation calculator API.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
amount: Initial amount in TRY
|
|
118
|
+
start: Start date in YYYY-MM format (e.g., "2020-01")
|
|
119
|
+
end: End date in YYYY-MM format (e.g., "2024-01")
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dictionary with:
|
|
123
|
+
- start_date: Start date
|
|
124
|
+
- end_date: End date
|
|
125
|
+
- initial_value: Initial amount
|
|
126
|
+
- final_value: Inflation-adjusted value
|
|
127
|
+
- total_years: Total years elapsed
|
|
128
|
+
- total_months: Total months elapsed
|
|
129
|
+
- total_change: Total percentage change
|
|
130
|
+
- avg_yearly_inflation: Average yearly inflation rate
|
|
131
|
+
- start_cpi: CPI at start date
|
|
132
|
+
- end_cpi: CPI at end date
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
>>> inf = Inflation()
|
|
136
|
+
>>> result = inf.calculate(100000, "2020-01", "2024-01")
|
|
137
|
+
>>> print(f"100,000 TL in 2020 = {result['final_value']:,.0f} TL in 2024")
|
|
138
|
+
100,000 TL in 2020 = 342,515 TL in 2024
|
|
139
|
+
"""
|
|
140
|
+
start_year, start_month = self._parse_year_month(start)
|
|
141
|
+
end_year, end_month = self._parse_year_month(end)
|
|
142
|
+
|
|
143
|
+
return self._provider.calculate_inflation(
|
|
144
|
+
start_year=start_year,
|
|
145
|
+
start_month=start_month,
|
|
146
|
+
end_year=end_year,
|
|
147
|
+
end_month=end_month,
|
|
148
|
+
basket_value=amount,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _parse_year_month(self, date_str: str) -> tuple[int, int]:
|
|
152
|
+
"""Parse YYYY-MM format to (year, month) tuple."""
|
|
153
|
+
try:
|
|
154
|
+
parts = date_str.split("-")
|
|
155
|
+
if len(parts) != 2:
|
|
156
|
+
raise ValueError(f"Invalid date format: {date_str}. Use YYYY-MM")
|
|
157
|
+
year = int(parts[0])
|
|
158
|
+
month = int(parts[1])
|
|
159
|
+
if not (1 <= month <= 12):
|
|
160
|
+
raise ValueError(f"Invalid month: {month}")
|
|
161
|
+
return year, month
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise ValueError(f"Could not parse date '{date_str}': {e}") from e
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return "Inflation()"
|
borsapy/market.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Market-level functions for BIST data."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from borsapy._providers.kap import get_kap_provider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def companies() -> pd.DataFrame:
|
|
9
|
+
"""
|
|
10
|
+
Get list of all BIST companies.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
DataFrame with columns:
|
|
14
|
+
- ticker: Stock ticker code (e.g., "THYAO", "GARAN")
|
|
15
|
+
- name: Company name
|
|
16
|
+
- city: Company headquarters city
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
>>> import borsapy as bp
|
|
20
|
+
>>> bp.companies()
|
|
21
|
+
ticker name city
|
|
22
|
+
0 ACSEL ACIPAYAM SELULOZ SANAYI A.S. DENIZLI
|
|
23
|
+
1 ADEL ADEL KALEMCILIK A.S. ISTANBUL
|
|
24
|
+
...
|
|
25
|
+
"""
|
|
26
|
+
provider = get_kap_provider()
|
|
27
|
+
return provider.get_companies()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def search_companies(query: str) -> pd.DataFrame:
|
|
31
|
+
"""
|
|
32
|
+
Search BIST companies by name or ticker.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query: Search query (ticker code or company name)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
DataFrame with matching companies, sorted by relevance.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
>>> import borsapy as bp
|
|
42
|
+
>>> bp.search_companies("THYAO")
|
|
43
|
+
ticker name city
|
|
44
|
+
0 THYAO TURK HAVA YOLLARI A.O. ISTANBUL
|
|
45
|
+
|
|
46
|
+
>>> bp.search_companies("banka")
|
|
47
|
+
ticker name city
|
|
48
|
+
0 GARAN TURKIYE GARANTI BANKASI A.S. ISTANBUL
|
|
49
|
+
1 AKBNK AKBANK T.A.S. ISTANBUL
|
|
50
|
+
...
|
|
51
|
+
"""
|
|
52
|
+
provider = get_kap_provider()
|
|
53
|
+
return provider.search(query)
|
borsapy/multi.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Multi-ticker functions and classes - yfinance-like API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from borsapy._providers.paratic import get_paratic_provider
|
|
8
|
+
from borsapy.ticker import Ticker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Tickers:
|
|
12
|
+
"""
|
|
13
|
+
Container for multiple Ticker objects.
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
>>> import borsapy as bp
|
|
17
|
+
>>> tickers = bp.Tickers("THYAO GARAN AKBNK")
|
|
18
|
+
>>> tickers.tickers["THYAO"].info
|
|
19
|
+
>>> tickers.symbols
|
|
20
|
+
['THYAO', 'GARAN', 'AKBNK']
|
|
21
|
+
|
|
22
|
+
>>> tickers = bp.Tickers(["THYAO", "GARAN", "AKBNK"])
|
|
23
|
+
>>> for symbol, ticker in tickers:
|
|
24
|
+
... print(symbol, ticker.info['last'])
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, symbols: str | list[str]):
|
|
28
|
+
"""
|
|
29
|
+
Initialize Tickers with multiple symbols.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
symbols: Space-separated string or list of symbols.
|
|
33
|
+
Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN", "AKBNK"]
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(symbols, str):
|
|
36
|
+
self._symbols = [s.strip().upper() for s in symbols.split() if s.strip()]
|
|
37
|
+
else:
|
|
38
|
+
self._symbols = [s.strip().upper() for s in symbols if s.strip()]
|
|
39
|
+
|
|
40
|
+
self._tickers: dict[str, Ticker] = {}
|
|
41
|
+
for symbol in self._symbols:
|
|
42
|
+
self._tickers[symbol] = Ticker(symbol)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def symbols(self) -> list[str]:
|
|
46
|
+
"""Return list of symbols."""
|
|
47
|
+
return self._symbols.copy()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def tickers(self) -> dict[str, Ticker]:
|
|
51
|
+
"""Return dictionary of Ticker objects keyed by symbol."""
|
|
52
|
+
return self._tickers
|
|
53
|
+
|
|
54
|
+
def history(
|
|
55
|
+
self,
|
|
56
|
+
period: str = "1mo",
|
|
57
|
+
interval: str = "1d",
|
|
58
|
+
start: datetime | str | None = None,
|
|
59
|
+
end: datetime | str | None = None,
|
|
60
|
+
group_by: str = "column",
|
|
61
|
+
) -> pd.DataFrame:
|
|
62
|
+
"""
|
|
63
|
+
Get historical data for all tickers.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
period: Data period (1d, 5d, 1mo, 3mo, 6mo, 1y, etc.)
|
|
67
|
+
interval: Data interval (1d, 1wk, 1mo)
|
|
68
|
+
start: Start date
|
|
69
|
+
end: End date
|
|
70
|
+
group_by: How to group columns ('column' or 'ticker')
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
DataFrame with multi-level columns.
|
|
74
|
+
"""
|
|
75
|
+
return download(
|
|
76
|
+
self._symbols,
|
|
77
|
+
period=period,
|
|
78
|
+
interval=interval,
|
|
79
|
+
start=start,
|
|
80
|
+
end=end,
|
|
81
|
+
group_by=group_by,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __iter__(self):
|
|
85
|
+
"""Iterate over (symbol, ticker) pairs."""
|
|
86
|
+
return iter(self._tickers.items())
|
|
87
|
+
|
|
88
|
+
def __len__(self) -> int:
|
|
89
|
+
"""Return number of tickers."""
|
|
90
|
+
return len(self._tickers)
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, symbol: str) -> Ticker:
|
|
93
|
+
"""Get ticker by symbol."""
|
|
94
|
+
symbol = symbol.upper()
|
|
95
|
+
if symbol not in self._tickers:
|
|
96
|
+
raise KeyError(f"Symbol not found: {symbol}")
|
|
97
|
+
return self._tickers[symbol]
|
|
98
|
+
|
|
99
|
+
def __repr__(self) -> str:
|
|
100
|
+
return f"Tickers({self._symbols})"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def download(
|
|
104
|
+
tickers: str | list[str],
|
|
105
|
+
period: str = "1mo",
|
|
106
|
+
interval: str = "1d",
|
|
107
|
+
start: datetime | str | None = None,
|
|
108
|
+
end: datetime | str | None = None,
|
|
109
|
+
group_by: str = "column",
|
|
110
|
+
progress: bool = True,
|
|
111
|
+
) -> pd.DataFrame:
|
|
112
|
+
"""
|
|
113
|
+
Download historical data for multiple tickers.
|
|
114
|
+
|
|
115
|
+
Similar to yfinance.download(), this function fetches OHLCV data
|
|
116
|
+
for multiple stocks and returns a DataFrame with multi-level columns.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
tickers: Space-separated string or list of symbols.
|
|
120
|
+
Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN"]
|
|
121
|
+
period: Data period. Valid values:
|
|
122
|
+
1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
|
|
123
|
+
Ignored if start is provided.
|
|
124
|
+
interval: Data interval. Valid values:
|
|
125
|
+
1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo.
|
|
126
|
+
start: Start date (string YYYY-MM-DD or datetime).
|
|
127
|
+
end: End date (string YYYY-MM-DD or datetime). Defaults to today.
|
|
128
|
+
group_by: How to group the output columns:
|
|
129
|
+
- 'column': MultiIndex (Price, Symbol) - default
|
|
130
|
+
- 'ticker': MultiIndex (Symbol, Price)
|
|
131
|
+
progress: Show progress (not implemented, for yfinance compatibility).
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
DataFrame with OHLCV data.
|
|
135
|
+
- If single ticker: Simple columns (Open, High, Low, Close, Volume)
|
|
136
|
+
- If multiple tickers: MultiIndex columns based on group_by
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
>>> import borsapy as bp
|
|
140
|
+
|
|
141
|
+
# Single ticker (returns simple DataFrame)
|
|
142
|
+
>>> bp.download("THYAO", period="1mo")
|
|
143
|
+
Open High Low Close Volume
|
|
144
|
+
Date
|
|
145
|
+
2024-12-01 265.00 268.00 264.00 267.50 12345678
|
|
146
|
+
|
|
147
|
+
# Multiple tickers (returns MultiIndex DataFrame)
|
|
148
|
+
>>> bp.download(["THYAO", "GARAN"], period="1mo")
|
|
149
|
+
Open High ...
|
|
150
|
+
THYAO GARAN THYAO GARAN
|
|
151
|
+
Date
|
|
152
|
+
2024-12-01 265.00 45.50 268.00 46.20
|
|
153
|
+
|
|
154
|
+
# With date range
|
|
155
|
+
>>> bp.download("THYAO GARAN AKBNK", start="2024-01-01", end="2024-06-30")
|
|
156
|
+
|
|
157
|
+
# Group by ticker
|
|
158
|
+
>>> bp.download(["THYAO", "GARAN"], group_by="ticker")
|
|
159
|
+
THYAO GARAN
|
|
160
|
+
Open High Low Close Open High
|
|
161
|
+
Date
|
|
162
|
+
2024-12-01 265.00 268.00 ... 45.50 46.20
|
|
163
|
+
"""
|
|
164
|
+
# Parse symbols
|
|
165
|
+
if isinstance(tickers, str):
|
|
166
|
+
symbols = [s.strip().upper() for s in tickers.split() if s.strip()]
|
|
167
|
+
else:
|
|
168
|
+
symbols = [s.strip().upper() for s in tickers if s.strip()]
|
|
169
|
+
|
|
170
|
+
if not symbols:
|
|
171
|
+
raise ValueError("No symbols provided")
|
|
172
|
+
|
|
173
|
+
# Parse dates
|
|
174
|
+
start_dt = _parse_date(start) if start else None
|
|
175
|
+
end_dt = _parse_date(end) if end else None
|
|
176
|
+
|
|
177
|
+
provider = get_paratic_provider()
|
|
178
|
+
|
|
179
|
+
# Fetch data for each symbol
|
|
180
|
+
data_frames: dict[str, pd.DataFrame] = {}
|
|
181
|
+
for symbol in symbols:
|
|
182
|
+
try:
|
|
183
|
+
df = provider.get_history(
|
|
184
|
+
symbol=symbol,
|
|
185
|
+
period=period,
|
|
186
|
+
interval=interval,
|
|
187
|
+
start=start_dt,
|
|
188
|
+
end=end_dt,
|
|
189
|
+
)
|
|
190
|
+
if not df.empty:
|
|
191
|
+
data_frames[symbol] = df
|
|
192
|
+
except Exception:
|
|
193
|
+
# Skip failed symbols silently (yfinance behavior)
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if not data_frames:
|
|
197
|
+
return pd.DataFrame()
|
|
198
|
+
|
|
199
|
+
# Single ticker - return simple DataFrame
|
|
200
|
+
if len(symbols) == 1 and len(data_frames) == 1:
|
|
201
|
+
return list(data_frames.values())[0]
|
|
202
|
+
|
|
203
|
+
# Multiple tickers - create MultiIndex DataFrame
|
|
204
|
+
if group_by == "ticker":
|
|
205
|
+
# Group by ticker first: (THYAO, Open), (THYAO, High), ...
|
|
206
|
+
result = pd.concat(data_frames, axis=1)
|
|
207
|
+
# result columns are already (symbol, price)
|
|
208
|
+
else:
|
|
209
|
+
# Group by column first: (Open, THYAO), (Open, GARAN), ...
|
|
210
|
+
result = pd.concat(data_frames, axis=1)
|
|
211
|
+
# Swap levels to get (price, symbol)
|
|
212
|
+
result = result.swaplevel(axis=1)
|
|
213
|
+
result = result.sort_index(axis=1, level=0)
|
|
214
|
+
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_date(date: str | datetime) -> datetime:
|
|
219
|
+
"""Parse a date string to datetime."""
|
|
220
|
+
if isinstance(date, datetime):
|
|
221
|
+
return date
|
|
222
|
+
for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
|
|
223
|
+
try:
|
|
224
|
+
return datetime.strptime(date, fmt)
|
|
225
|
+
except ValueError:
|
|
226
|
+
continue
|
|
227
|
+
raise ValueError(f"Could not parse date: {date}")
|