PYield 0.34.2__tar.gz → 0.35.1__tar.gz

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.
Files changed (51) hide show
  1. {pyield-0.34.2 → pyield-0.35.1}/PKG-INFO +1 -1
  2. pyield-0.35.1/pyield/__about__.py +1 -0
  3. {pyield-0.34.2 → pyield-0.35.1}/pyield/__init__.py +2 -2
  4. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/__init__.py +2 -2
  5. pyield-0.34.2/pyield/b3/dif.py → pyield-0.35.1/pyield/b3/di1.py +52 -80
  6. {pyield-0.34.2 → pyield-0.35.1}/pyield/data_cache.py +9 -1
  7. {pyield-0.34.2 → pyield-0.35.1}/pyield/retry.py +64 -44
  8. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ltn.py +2 -2
  9. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ntnb.py +1 -1
  10. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ntnf.py +2 -2
  11. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/tools.py +2 -2
  12. pyield-0.34.2/pyield/__about__.py +0 -1
  13. {pyield-0.34.2 → pyield-0.35.1}/.gitignore +0 -0
  14. {pyield-0.34.2 → pyield-0.35.1}/LICENSE +0 -0
  15. {pyield-0.34.2 → pyield-0.35.1}/README.md +0 -0
  16. {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/__init__.py +0 -0
  17. {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/ettj.py +0 -0
  18. {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/ima.py +0 -0
  19. {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/imaq.py +0 -0
  20. {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/ipca.py +0 -0
  21. {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/tpf.py +0 -0
  22. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/di_over.py +0 -0
  23. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/__init__.py +0 -0
  24. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/common.py +0 -0
  25. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/historical.py +0 -0
  26. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/intraday.py +0 -0
  27. {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/xml.py +0 -0
  28. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/__init__.py +0 -0
  29. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/auction.py +0 -0
  30. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/bcdata.py +0 -0
  31. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/ptax.py +0 -0
  32. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/repo.py +0 -0
  33. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/trades_intraday.py +0 -0
  34. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/trades_monthly.py +0 -0
  35. {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/vna.py +0 -0
  36. {pyield-0.34.2 → pyield-0.35.1}/pyield/bday.py +0 -0
  37. {pyield-0.34.2 → pyield-0.35.1}/pyield/conftest.py +0 -0
  38. {pyield-0.34.2 → pyield-0.35.1}/pyield/date_converter.py +0 -0
  39. {pyield-0.34.2 → pyield-0.35.1}/pyield/fwd.py +0 -0
  40. {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/__init__.py +0 -0
  41. {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/br_holidays_new.txt +0 -0
  42. {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/br_holidays_old.txt +0 -0
  43. {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/brholidays.py +0 -0
  44. {pyield-0.34.2 → pyield-0.35.1}/pyield/ibge/__init__.py +0 -0
  45. {pyield-0.34.2 → pyield-0.35.1}/pyield/ibge/ipca.py +0 -0
  46. {pyield-0.34.2 → pyield-0.35.1}/pyield/interpolator.py +0 -0
  47. {pyield-0.34.2 → pyield-0.35.1}/pyield/py.typed +0 -0
  48. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/__init__.py +0 -0
  49. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/lft.py +0 -0
  50. {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ntnc.py +0 -0
  51. {pyield-0.34.2 → pyield-0.35.1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.34.2
3
+ Version: 0.35.1
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Source, https://github.com/crdcj/PYield
6
6
  Author-email: Carlos Carvalho <cr.cj@outlook.com>
@@ -0,0 +1 @@
1
+ __version__ = "0.35.1"
@@ -2,7 +2,7 @@ import logging
2
2
 
3
3
  from pyield import anbima, b3, bc, bday, ibge
4
4
  from pyield.__about__ import __version__
5
- from pyield.b3 import dif, futures
5
+ from pyield.b3 import di1, futures
6
6
  from pyield.fwd import forward, forwards
7
7
  from pyield.interpolator import Interpolator
8
8
  from pyield.tpf import lft, ltn, ntnb, ntnc, ntnf
@@ -11,7 +11,7 @@ __all__ = [
11
11
  "__version__",
12
12
  "anbima",
13
13
  "bday",
14
- "dif",
14
+ "di1",
15
15
  "forwards",
16
16
  "forward",
17
17
  "futures",
@@ -1,9 +1,9 @@
1
- from pyield.b3 import dif
1
+ from pyield.b3 import di1
2
2
  from pyield.b3.di_over import di_over
3
3
  from pyield.b3.futures import futures
4
4
 
5
5
  __all__ = [
6
6
  "di_over",
7
7
  "futures",
8
- "dif",
8
+ "di1",
9
9
  ]
@@ -15,7 +15,7 @@ TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
15
15
 
16
16
 
17
17
  def data(
18
- date: DateScalar,
18
+ dates: DateScalar | DateArray,
19
19
  month_start: bool = False,
20
20
  pre_filter: bool = False,
21
21
  all_columns: bool = True,
@@ -27,9 +27,7 @@ def data(
27
27
  (to month start) and optional filtering based on LTN and NTN-F bond maturities.
28
28
 
29
29
  Args:
30
- date (DateScalar): The trade date for which to retrieve DI contract data.
31
- If the date is invalid, a holiday, in the future, or None, an empty
32
- DataFrame is returned with appropriate logging.
30
+ dates (DateScalar): The trade dates for which to retrieve DI contract data.
33
31
  month_start (bool, optional): If True, adjusts all expiration dates to the
34
32
  first day of their respective month (e.g., 2025-02-01 becomes
35
33
  2025-01-01). Defaults to False.
@@ -43,12 +41,12 @@ def data(
43
41
 
44
42
  Returns:
45
43
  pd.DataFrame: A DataFrame containing the DI futures contract data for the
46
- specified date, sorted by expiration date. Returns an empty DataFrame
47
- if no data is found, the date is invalid, a holiday, or in the future.
44
+ specified dates, sorted by trade dates and expiration dates.
45
+ Returns an empty DataFrame if no data is found
48
46
 
49
47
  Examples:
50
- >>> from pyield import dif
51
- >>> df = dif.data(date="16-10-2024", month_start=True)
48
+ >>> from pyield import di1
49
+ >>> df = di1.data(dates="16-10-2024", month_start=True)
52
50
  >>> df.iloc[:5, :5] # Show the first five rows and columns
53
51
  TradeDate ExpirationDate TickerSymbol BDaysToExp OpenContracts
54
52
  0 2024-10-16 2024-11-01 DI1X24 12 1744269
@@ -57,31 +55,25 @@ def data(
57
55
  3 2024-10-16 2025-02-01 DI1G25 74 279491
58
56
  4 2024-10-16 2025-03-01 DI1H25 94 344056
59
57
  """
60
- if date:
61
- date = dc.convert_input_dates(date)
62
- bz_today = dt.now(TIMEZONE_BZ).date()
63
- if date.date() > bz_today:
64
- logger.warning(f"DI date ({date}) after current date {bz_today}")
65
- logger.warning("Returning empty DataFrame.")
66
- return pd.DataFrame()
67
- else:
68
- logger.info("No date specified. Returning empty DataFrame.")
69
- return pd.DataFrame()
70
- # Return an empty DataFrame if the trade date is a holiday
71
- if not bday.is_business_day(date):
72
- logger.warning("Specified date is not a business day.")
73
- logger.warning("Returning empty DataFrame.")
74
- return pd.DataFrame()
75
58
 
76
- # Get historical data
77
- df = get_cached_dataset("DI").query("TradeDate == @date").reset_index(drop=True)
59
+ dates = dc.convert_input_dates(dates)
60
+ # Force dates to be a Series for consistency
61
+ dates = pd.Series(dates) if isinstance(dates, pd.Timestamp) else dates
62
+ # Remove duplicates and sort dates
63
+ dates = dates.drop_duplicates().sort_values().reset_index(drop=True)
78
64
 
79
- if df.empty:
80
- logger.info("No historical data found. Trying real-time data.")
81
- df = b3.futures(contract_code="DI1", date=date)
65
+ df = get_cached_dataset("DI").query("TradeDate in @dates").reset_index(drop=True)
66
+
67
+ today = dt.now(TIMEZONE_BZ)
68
+ last_bday = bday.last_business_day()
69
+ is_trading_day = today.date() == last_bday.date()
70
+ if is_trading_day and last_bday in df["TradeDate"]:
71
+ logger.info("Trying to get DI Futures data for today.")
72
+ df_intraday = b3.futures(contract_code="DI1", date=last_bday)
73
+ df = pd.concat([df, df_intraday]).reset_index(drop=True)
82
74
 
83
75
  if df.empty:
84
- logger.warning("No DI Futures data found for the specified date.")
76
+ logger.warning("No DI Futures data found for the specified dates.")
85
77
  logger.warning("Returning empty DataFrame.")
86
78
  return pd.DataFrame()
87
79
 
@@ -92,24 +84,35 @@ def data(
92
84
  df_pre = (
93
85
  get_cached_dataset("TPF")
94
86
  .query("BondType in ['LTN', 'NTN-F']")
95
- .reset_index(drop=True)[["ReferenceDate", "MaturityDate"]]
87
+ .drop_duplicates(subset=["ReferenceDate", "MaturityDate"])
88
+ .sort_values(["ReferenceDate", "MaturityDate"])
89
+ .reset_index(drop=True)[["ReferenceDate", "MaturityDate", "BondType"]]
96
90
  )
91
+ # Assure that the MaturityDate is a business day
92
+ df_pre["MaturityDate"] = bday.offset(df_pre["MaturityDate"], 0)
97
93
 
98
- nearest_date = _find_nearest_date(date, df_pre["ReferenceDate"])
99
- if nearest_date is pd.NaT:
100
- logger.warning("No matching reference date found in TPF dataset.")
101
- return pd.DataFrame()
94
+ # Rename columns for merging
95
+ df_pre = df_pre.rename(
96
+ columns={"ReferenceDate": "TradeDate", "MaturityDate": "ExpirationDate"}
97
+ )
98
+
99
+ df = pd.merge_asof(
100
+ df, df_pre, on="TradeDate", by="ExpirationDate", direction="backward"
101
+ )
102
102
 
103
- # Filter the TPF dataset for the nearest reference date
104
- pre_maturities = (
105
- df_pre.query("ReferenceDate == @nearest_date")["MaturityDate"]
106
- .drop_duplicates()
103
+ # BondType was used as flag to filter the maturities
104
+ df = (
105
+ df.query("BondType.notna()")
106
+ .drop(columns=["BondType"])
107
107
  .reset_index(drop=True)
108
108
  )
109
109
 
110
- # Force the expirations to be a business day as DI contracts
111
- pre_maturities = bday.offset(pre_maturities, 0)
112
- df = df.query("ExpirationDate in @pre_maturities").reset_index(drop=True)
110
+ if df.empty:
111
+ logger.warning(
112
+ "No DI Futures data found for the specified dates after pre-filtering."
113
+ "Returning empty DataFrame."
114
+ )
115
+ return pd.DataFrame()
113
116
 
114
117
  if month_start:
115
118
  df["ExpirationDate"] = df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
@@ -139,33 +142,6 @@ def data(
139
142
  return df.sort_values(by=["TradeDate", "ExpirationDate"]).reset_index(drop=True)
140
143
 
141
144
 
142
- def _find_nearest_date(
143
- target_date: pd.Timestamp, date_series: pd.Series
144
- ) -> pd.Timestamp:
145
- """Finds the date in a Series closest to the target date.
146
-
147
- Args:
148
- target_date (pd.Timestamp): The reference date.
149
- date_series (pd.Series): A Series of dates to search within.
150
-
151
- Returns:
152
- pd.Timestamp: The date from `date_series` that is closest in time
153
- to `target_date`. Returns pd.NaT if the series is empty or
154
- target_date is None.
155
- """
156
- if date_series.empty or target_date is None:
157
- return pd.NaT
158
- # The result will be a Series of positive Timedeltas (durations)
159
- abs_differences = (date_series - target_date).abs()
160
-
161
- # Find the index of the minimum difference in the differences Series
162
- # idxmin() returns the index label where the minimum value occurs
163
- closest_index = abs_differences.idxmin()
164
-
165
- # Use the found index to get the corresponding date from the original Series
166
- return date_series.loc[closest_index] # or date_series[closest_index]
167
-
168
-
169
145
  def interpolate_rates(
170
146
  dates: DateScalar | DateArray,
171
147
  expirations: DateScalar | DateArray,
@@ -230,11 +206,7 @@ def interpolate_rates(
230
206
  dfi["irate"] = dfi["irate"].astype("Float64")
231
207
 
232
208
  # Load DI rates dataset filtered by the provided reference dates
233
- dfr = (
234
- get_cached_dataset("DI")
235
- .query("TradeDate in @dfi['tdate'].unique()")
236
- .reset_index(drop=True)
237
- )
209
+ dfr = data(dates=dates)
238
210
 
239
211
  # Return an empty DataFrame if no rates are found
240
212
  if dfr.empty:
@@ -295,17 +267,17 @@ def interpolate_rate(
295
267
  - An interpolation calculation fails.
296
268
 
297
269
  Examples:
298
- >>> from pyield import dif
270
+ >>> from pyield import di1
299
271
  >>> # Get rate for an existing contract expiration
300
- >>> dif.interpolate_rate("25-04-2025", "01-01-2027")
272
+ >>> di1.interpolate_rate("25-04-2025", "01-01-2027")
301
273
  0.13901
302
274
 
303
275
  >>> # Get rate for a non-existing contract expiration
304
- >>> dif.interpolate_rate("25-04-2025", "01-11-2027")
276
+ >>> di1.interpolate_rate("25-04-2025", "01-11-2027")
305
277
  0.13576348733268917
306
278
 
307
279
  >>> # Extrapolate rate for a future expiration date
308
- >>> dif.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
280
+ >>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
309
281
  0.13881
310
282
  """
311
283
 
@@ -314,7 +286,7 @@ def interpolate_rate(
314
286
  return float("NaN")
315
287
 
316
288
  # Get the DI contract DataFrame
317
- df = data(date=date)
289
+ df = data(dates=date)
318
290
 
319
291
  if df.empty:
320
292
  return float("NaN")
@@ -354,9 +326,9 @@ def eod_dates() -> pd.Series:
354
326
  for which DI data is available.
355
327
 
356
328
  Examples:
357
- >>> from pyield import dif
329
+ >>> from pyield import di1
358
330
  >>> # DI Futures series starts from 1995-01-02
359
- >>> dif.eod_dates().head(5)
331
+ >>> di1.eod_dates().head(5)
360
332
  0 1995-01-02
361
333
  1 1995-01-03
362
334
  2 1995-01-04
@@ -5,6 +5,8 @@ from zoneinfo import ZoneInfo
5
5
 
6
6
  import pandas as pd
7
7
 
8
+ from pyield.retry import default_retry
9
+
8
10
  # --- Configurações Centralizadas ---
9
11
  TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
10
12
 
@@ -40,6 +42,12 @@ def _get_today_date_key():
40
42
  return datetime.now(TIMEZONE_BZ).strftime("%Y-%m-%d")
41
43
 
42
44
 
45
+ @default_retry
46
+ def _load_github_file(file_url: str) -> pd.DataFrame:
47
+ """Carrega um arquivo do GitHub e retorna um DataFrame."""
48
+ return pd.read_parquet(file_url)
49
+
50
+
43
51
  @functools.lru_cache(maxsize=len(DATASET_CONFIGS))
44
52
  def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pd.DataFrame:
45
53
  """Carrega o dataset a partir do ID e da chave de data. Cache expira diariamente."""
@@ -55,7 +63,7 @@ def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pd.DataFrame:
55
63
  full_url = f"{BASE_DATA_URL}/{filename}"
56
64
 
57
65
  try:
58
- df = pd.read_parquet(full_url)
66
+ df = _load_github_file(full_url)
59
67
  last_date = df[date_column].max().date()
60
68
  logger.info(f"Dataset {dataset_name} carregado. Última data: {last_date}")
61
69
  return df.copy()
@@ -19,66 +19,65 @@ from tenacity import RetryCallState
19
19
  # --- Configuração do Logger ---
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
- # --- Constantes ---
23
- DEFAULT_TIMEOUT = (2, 10)
24
- RETRYABLE_NETWORK_ERRORS = (URLError,)
25
- RETRYABLE_PARSING_ERRORS = (pd.errors.EmptyDataError, pd.errors.ParserError)
26
-
27
22
 
28
23
  # --- Funções Auxiliares para Tenacity ---
29
-
30
-
31
24
  def _log_before_sleep(retry_state: RetryCallState):
32
25
  """
33
26
  Loga uma mensagem ANTES de o Tenacity entrar em espera (sleep) entre tentativas.
34
-
35
- Simplificada para confiar no contrato do Tenacity e usar asserções
36
- para verificar o estado esperado.
37
27
  """
38
- # 1. Verificar o resultado (outcome)
39
28
  outcome = retry_state.outcome
40
- assert outcome is not None, "before_sleep chamada sem 'outcome'"
41
- # Adicionado: Garantir que realmente falhou antes de logar como falha
42
- assert outcome.failed, "before_sleep chamada para 'outcome' bem-sucedido"
29
+ if not outcome or not outcome.failed:
30
+ logger.debug(
31
+ "before_sleep chamada sem 'outcome' ou para 'outcome' bem-sucedido."
32
+ )
33
+ return
43
34
 
44
- exception = outcome.exception
45
- # Adicionado: Garantir que a exceção existe quando falhou
46
- assert exception is not None, "before_sleep chamada sem 'exception' no outcome"
35
+ exception = outcome.exception()
36
+ if not exception:
37
+ logger.debug("before_sleep chamada sem 'exception' real no outcome.")
38
+ return
47
39
 
48
- # 2. Verificar a próxima ação (next_action)
49
40
  next_action = retry_state.next_action
50
- assert next_action is not None, "before_sleep chamada sem 'next_action'"
41
+ if not next_action or not hasattr(next_action, "sleep"):
42
+ logger.debug(
43
+ "before_sleep chamada sem 'next_action' ou 'sleep' em next_action."
44
+ )
45
+ return
51
46
 
52
- # 3. Acessar a duração do sono (sleep)
53
- # Confiamos que 'next_action' terá '.sleep' neste contexto, pois é antes de dormir.
54
47
  sleep_duration = next_action.sleep
55
48
 
56
- # Log principal
57
49
  logger.warning(
58
50
  f"Tentativa {retry_state.attempt_number} falhou com "
59
- f"{type(exception).__name__}: {exception}. Tentando novamente em "
51
+ f"{type(exception).__name__}: {str(exception)[:150]}. Tentando novamente em "
60
52
  f"{sleep_duration:.2f} segundos..."
61
53
  )
62
54
 
63
55
 
64
56
  def should_retry_exception(retry_state: RetryCallState) -> bool:
65
- """Determina se uma exceção capturada justifica uma nova tentativa."""
57
+ """Determina se uma exceção capturada justifica uma nova tentativa"""
66
58
  if not retry_state.outcome or not retry_state.outcome.failed:
67
59
  return False
68
60
 
69
- # Usar o atributo .exception
70
- exception = retry_state.outcome.exception
71
- assert exception is not None, (
72
- "should_retry_exception processando um outcome sem exceção?"
73
- )
61
+ exception = retry_state.outcome.exception()
62
+
63
+ if exception is None:
64
+ # Isso não deveria acontecer se outcome.failed é True.
65
+ logger.error(
66
+ "should_retry_exception: outcome.failed é True,"
67
+ "mas exception() retornou None."
68
+ )
69
+ return False
74
70
 
75
71
  attempt_number = retry_state.attempt_number
72
+ # getattr é mais seguro caso a exceção não tenha 'url'
76
73
  url = getattr(exception, "url", "N/A")
77
74
 
78
75
  retry_decision = False
79
- log_message = ""
80
- log_level = logging.ERROR
76
+ # Inicializar como None para evitar log default se nenhum case corresponder
77
+ log_message = None
78
+ log_level = logging.ERROR # Default para não retentativa
81
79
 
80
+ # Usando match/case como originalmente preferido
82
81
  match exception:
83
82
  case HTTPError(code=404):
84
83
  log_message = (
@@ -88,6 +87,15 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
88
87
  retry_decision = False
89
88
  log_level = logging.INFO
90
89
 
90
+ case HTTPError(code=429): # ADICIONADO: Tratamento para Too Many Requests
91
+ log_message = (
92
+ f"Tentativa {attempt_number}: Too Many Requests (429) da API em {url}. "
93
+ f"API pediu para aguardar. Tentando novamente."
94
+ )
95
+ retry_decision = True
96
+ log_level = logging.WARNING
97
+
98
+ # Gateway Timeout, primeira tentativa
91
99
  case HTTPError(code=504) if attempt_number == 1:
92
100
  log_message = (
93
101
  f"Tentativa {attempt_number}: Gateway Timeout (504) da API em {url}. "
@@ -96,7 +104,7 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
96
104
  retry_decision = True
97
105
  log_level = logging.WARNING
98
106
 
99
- case HTTPError(code=504):
107
+ case HTTPError(code=504): # Gateway Timeout, tentativas subsequentes
100
108
  log_message = (
101
109
  f"Tentativa {attempt_number}: Gateway Timeout (504) persistiu em {url}."
102
110
  f"API parece indisponível. Desistindo."
@@ -104,41 +112,52 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
104
112
  retry_decision = False
105
113
  log_level = logging.ERROR
106
114
 
107
- case HTTPError() as http_err:
115
+ case HTTPError() as http_err: # Outros erros HTTP
116
+ # Esta regra genérica para HTTPError retentará outros códigos HTTP.
117
+ # Por exemplo, 500, 502, 503 (geralmente bom para retry)
118
+ # mas também 400, 401, 403 (geralmente não bom para retry).
119
+ # Se precisar de mais granularidade, adicione cases específicos acima deste.
108
120
  log_message = (
109
- f"Tentativa {attempt_number}: Erro HTTP {http_err.code} encontrado "
110
- f"para {url}. Pode ser transitório. Tentando novamente."
121
+ f"Tentativa {attempt_number}: Erro HTTP {http_err.code} "
122
+ f"({http_err.reason}) encontrado para {url}. "
123
+ "Pode ser transitório. Tentando novamente."
111
124
  )
112
125
  retry_decision = True
113
126
  log_level = logging.WARNING
114
127
 
128
+ # Erros de rede mais genéricos (e.g., DNS falhou, conexão recusada)
115
129
  case URLError():
116
130
  log_message = (
117
131
  f"Tentativa {attempt_number}: Erro de rede recuperável "
118
- f"({type(exception).__name__}). Tentando novamente."
132
+ f"({type(exception).__name__}: {str(exception)[:100]}) para {url}. "
133
+ f"Tentando novamente."
119
134
  )
120
135
  retry_decision = True
121
136
  log_level = logging.WARNING
122
137
 
138
+ # Usando o operador | para combinar tipos de exceção no case
123
139
  case pd.errors.EmptyDataError() | pd.errors.ParserError():
124
140
  log_message = (
125
- f"Tentativa {attempt_number}: Erro de parsing "
141
+ f"Tentativa {attempt_number}: Erro de parsing de dados "
126
142
  f"({type(exception).__name__}). Tentando novamente."
127
143
  )
128
144
  retry_decision = True
129
145
  log_level = logging.WARNING
130
146
 
131
- case _:
147
+ case _: # Exceção não mapeada/inesperada
148
+ # Mensagem para o log de exceção
132
149
  log_message_exc = (
133
150
  f"Tentativa {attempt_number}: Erro não recuperável/inesperado "
134
- f"({type(exception).__name__}). Desistindo."
151
+ f"({type(exception).__name__}: {str(exception)[:100]}) "
152
+ f"ao processar {url}. Desistindo."
135
153
  )
136
- retry_decision = False
137
- log_level = logging.ERROR
154
+ # Sempre incluir traceback para erros inesperados
138
155
  logger.exception(log_message_exc)
139
- log_message = None
156
+ log_message = None # Evita log duplicado pela seção logger.log abaixo
157
+ retry_decision = False
158
+ log_level = logging.ERROR # Já é o default, mas explícito aqui
140
159
 
141
- if log_message:
160
+ if log_message: # Loga a mensagem formatada se uma foi definida
142
161
  logger.log(log_level, log_message)
143
162
 
144
163
  return retry_decision
@@ -147,8 +166,9 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
147
166
  # --- Decorador Tenacity Principal ---
148
167
  default_retry = tenacity.retry(
149
168
  retry=should_retry_exception,
169
+ # Backoff exponencial é bom para 429
150
170
  wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
151
171
  stop=(tenacity.stop_after_attempt(3) | tenacity.stop_after_delay(15)),
152
172
  before_sleep=_log_before_sleep,
153
- reraise=True,
173
+ reraise=True, # Importante para propagar o erro final se as tentativas falharem
154
174
  )
@@ -2,7 +2,7 @@ import pandas as pd
2
2
 
3
3
  from pyield import anbima, bday
4
4
  from pyield import date_converter as dc
5
- from pyield.b3 import dif
5
+ from pyield.b3 import di1
6
6
  from pyield.date_converter import DateScalar
7
7
  from pyield.tpf import tools
8
8
 
@@ -223,7 +223,7 @@ def historical_premium(
223
223
  ltn_rate = float(ltn_rates.iloc[0])
224
224
 
225
225
  # Retrieve DI rate for the reference date and maturity
226
- di_rate = dif.interpolate_rate(date=date, expiration=maturity, extrapolate=True)
226
+ di_rate = di1.interpolate_rate(date=date, expiration=maturity, extrapolate=True)
227
227
  if pd.isnull(di_rate): # Check if the DI rate is NaN
228
228
  return float("NaN")
229
229
 
@@ -463,7 +463,7 @@ def bei_rates(
463
463
 
464
464
  Get the DI Futures settlement rates for the same reference date to be used as
465
465
  reference for the nominal rates:
466
- >>> df_di = yd.dif.data("05-09-2024")
466
+ >>> df_di = yd.di1.data("05-09-2024")
467
467
 
468
468
  Calculate the BEI rates considering the settlement at the reference date:
469
469
  >>> yd.ntnb.bei_rates(
@@ -6,7 +6,7 @@ import pandas as pd
6
6
  from pyield import anbima, bday
7
7
  from pyield import date_converter as dc
8
8
  from pyield import interpolator as ip
9
- from pyield.b3 import dif
9
+ from pyield.b3 import di1
10
10
  from pyield.date_converter import DateScalar
11
11
  from pyield.tpf import tools
12
12
 
@@ -596,7 +596,7 @@ def historical_premium(
596
596
  df["BYears"] = df["BDToMat"] / 252
597
597
  df["ReferenceDate"] = date
598
598
 
599
- df["DIRate"] = dif.interpolate_rates(
599
+ df["DIRate"] = di1.interpolate_rates(
600
600
  dates=df["ReferenceDate"],
601
601
  expirations=df["PaymentDate"],
602
602
  extrapolate=True,
@@ -5,7 +5,7 @@ import pandas as pd
5
5
 
6
6
  from pyield import date_converter as dc
7
7
  from pyield.anbima import tpf
8
- from pyield.b3 import dif
8
+ from pyield.b3 import di1
9
9
  from pyield.date_converter import DateScalar
10
10
 
11
11
 
@@ -77,7 +77,7 @@ def pre_spreads(date: DateScalar) -> pd.DataFrame:
77
77
  """
78
78
  # Fetch DI rates for the reference date
79
79
  converted_date = dc.convert_input_dates(date)
80
- df_di = dif.data(date=converted_date, month_start=True)
80
+ df_di = di1.data(dates=converted_date, month_start=True)
81
81
  if "SettlementRate" not in df_di.columns or df_di.empty:
82
82
  raise ValueError("DI data is missing the 'SettlementRate' column or is empty.")
83
83
 
@@ -1 +0,0 @@
1
- __version__ = "0.34.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes