1+ from __future__ import annotations
2+
13import re
24import threading
35from datetime import date , timedelta
@@ -359,6 +361,93 @@ def _fetch_symbol_response(
359361 return res
360362
361363
364+ def _validate_currency_csv (csv_text : str ) -> pd .DataFrame :
365+ """Parse and validate currency CSV format.
366+
367+ Parameters
368+ ----------
369+ csv_text : str
370+ CSV content from BCB API
371+
372+ Returns
373+ -------
374+ pd.DataFrame
375+ Parsed DataFrame with all columns
376+
377+ Raises
378+ ------
379+ BCBAPIError
380+ If CSV format is invalid (wrong column count)
381+ """
382+ df = pd .read_csv (StringIO (csv_text ), delimiter = ";" , header = None , dtype = str )
383+
384+ # Validate column count
385+ if len (df .columns ) != 8 :
386+ raise BCBAPIError (
387+ f"Invalid CSV format: expected 8 columns, got { len (df .columns )} " ,
388+ status_code = 400 ,
389+ )
390+
391+ # Assign meaningful names
392+ df .columns = ["Date" , "_col1" , "_col2" , "_col3" , "bid" , "ask" , "_col6" , "_col7" ]
393+ return df
394+
395+
396+ def _parse_currency_dates (df : pd .DataFrame ) -> pd .DataFrame :
397+ """Parse and validate date column in currency CSV.
398+
399+ Parameters
400+ ----------
401+ df : pd.DataFrame
402+ DataFrame with Date column as strings
403+
404+ Returns
405+ -------
406+ pd.DataFrame
407+ DataFrame with parsed DatetimeIndex
408+
409+ Raises
410+ ------
411+ BCBAPIError
412+ If date parsing fails
413+ """
414+ try :
415+ df ["Date" ] = pd .to_datetime (df ["Date" ], format = "%d%m%Y" )
416+ except ValueError as e :
417+ raise BCBAPIError (
418+ f"Failed to parse currency date column: { str (e )} " , status_code = 400
419+ )
420+ return df
421+
422+
423+ def _parse_currency_types (df : pd .DataFrame ) -> pd .DataFrame :
424+ """Parse and validate data types in currency DataFrame.
425+
426+ Parameters
427+ ----------
428+ df : pd.DataFrame
429+ DataFrame with mixed types
430+
431+ Returns
432+ -------
433+ pd.DataFrame
434+ DataFrame with validated types
435+
436+ Raises
437+ ------
438+ BCBAPIError
439+ If type conversion fails
440+ """
441+ try :
442+ df ["bid" ] = df ["bid" ].str .replace ("," , "." ).astype (np .float64 )
443+ df ["ask" ] = df ["ask" ].str .replace ("," , "." ).astype (np .float64 )
444+ except (ValueError , TypeError ) as e :
445+ raise BCBAPIError (
446+ f"Failed to parse currency numeric columns: { str (e )} " , status_code = 400
447+ )
448+ return df
449+
450+
362451def _get_symbol (
363452 symbol : str , start_date : DateInput , end_date : DateInput
364453) -> pd .DataFrame :
@@ -383,18 +472,12 @@ def _get_symbol(
383472 CurrencyNotFoundError
384473 If currency not found
385474 BCBAPIError
386- If API returns error
475+ If API returns error or data format is invalid
387476 """
388477 res = _fetch_symbol_response (symbol , start_date , end_date )
389- columns = ["Date" , "aa" , "bb" , "cc" , "bid" , "ask" , "dd" , "ee" ]
390- df = pd .read_csv (
391- StringIO (res .text ), delimiter = ";" , header = None , names = columns , dtype = str
392- )
393- df = df .assign (
394- Date = lambda x : pd .to_datetime (x ["Date" ], format = "%d%m%Y" ),
395- bid = lambda x : x ["bid" ].str .replace ("," , "." ).astype (np .float64 ),
396- ask = lambda x : x ["ask" ].str .replace ("," , "." ).astype (np .float64 ),
397- )
478+ df = _validate_currency_csv (res .text )
479+ df = _parse_currency_dates (df )
480+ df = _parse_currency_types (df )
398481 df1 = df .set_index ("Date" )
399482 n = ["bid" , "ask" ]
400483 df1 = df1 [n ]
@@ -431,9 +514,13 @@ def _get_symbol_text(symbol: str, start_date: DateInput, end_date: DateInput) ->
431514 return res .text
432515
433516
517+ # Type alias for text output with multiple symbols
518+ CurrencyTextResult = Dict [str , str ] # Maps symbol → CSV text
519+
520+
434521@overload
435522def get (
436- symbols : Union [ str , List [ str ]] ,
523+ symbols : str ,
437524 start : DateInput ,
438525 end : DateInput ,
439526 side : str = ...,
@@ -444,13 +531,35 @@ def get(
444531
445532@overload
446533def get (
447- symbols : Union [str , List [str ]],
534+ symbols : List [str ],
535+ start : DateInput ,
536+ end : DateInput ,
537+ side : str = ...,
538+ groupby : str = ...,
539+ output : Literal ["dataframe" ] = ...,
540+ ) -> pd .DataFrame : ...
541+
542+
543+ @overload
544+ def get (
545+ symbols : str ,
546+ start : DateInput ,
547+ end : DateInput ,
548+ side : str = ...,
549+ groupby : str = ...,
550+ output : Literal ["text" ] = ...,
551+ ) -> str : ...
552+
553+
554+ @overload
555+ def get (
556+ symbols : List [str ],
448557 start : DateInput ,
449558 end : DateInput ,
450559 side : str = ...,
451560 groupby : str = ...,
452561 output : Literal ["text" ] = ...,
453- ) -> Union [ str , Dict [ str , str ]] : ...
562+ ) -> CurrencyTextResult : ...
454563
455564
456565def get (
0 commit comments