data-retrieval-module 1.0.0__tar.gz → 1.0.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 (30) hide show
  1. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/CHANGELOG.md +23 -0
  2. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/PKG-INFO +2 -1
  3. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval/__init__.py +13 -1
  4. data_retrieval_module-1.0.1/data_retrieval/foreign_exchange/__init__.py +3 -0
  5. data_retrieval_module-1.0.1/data_retrieval/foreign_exchange/forex_data_provider_base.py +140 -0
  6. data_retrieval_module-1.0.1/data_retrieval/foreign_exchange/forex_data_provider_wrapper.py +148 -0
  7. data_retrieval_module-1.0.1/data_retrieval/foreign_exchange/forex_python_data_provider.py +161 -0
  8. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval/model/__init__.py +2 -1
  9. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval/model/data_provider.py +0 -1
  10. data_retrieval_module-1.0.1/data_retrieval/model/data_provider_wrapper.py +428 -0
  11. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval/model/exceptions.py +23 -2
  12. data_retrieval_module-1.0.1/data_retrieval/utils/__init__.py +0 -0
  13. data_retrieval_module-1.0.1/data_retrieval/utils/date_utils.py +54 -0
  14. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval_module.egg-info/PKG-INFO +2 -1
  15. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval_module.egg-info/SOURCES.txt +7 -0
  16. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval_module.egg-info/requires.txt +1 -0
  17. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/pyproject.toml +3 -2
  18. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/tests/test_basic.py +1 -1
  19. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/LICENSE +0 -0
  20. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/MANIFEST.in +0 -0
  21. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/Makefile +0 -0
  22. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/README.md +0 -0
  23. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval/model/data_module.py +0 -0
  24. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval/py.typed +0 -0
  25. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval_module.egg-info/dependency_links.txt +0 -0
  26. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/data_retrieval_module.egg-info/top_level.txt +0 -0
  27. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/requirements-test.txt +0 -0
  28. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/setup.cfg +0 -0
  29. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/setup.py +0 -0
  30. {data_retrieval_module-1.0.0 → data_retrieval_module-1.0.1}/tests/__init__.py +0 -0
@@ -42,6 +42,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
42
42
  - CI/CD ready setup
43
43
  - Package publishing configuration
44
44
 
45
+ ## [1.0.1] - 2026-01-25
46
+
47
+ ### Added
48
+ - Foreign Exchange data provider module
49
+ - `Forex_DataProvider_Base` abstract base class for forex data providers
50
+ - `ForexPython_DataProvider` concrete implementation using Python libraries
51
+ - `Forex_DataProvider_Wrapper` wrapper for enhanced functionality
52
+ - Utility modules
53
+ - `date_utils` module for date/time utilities
54
+ - Enhanced data provider wrapper functionality
55
+ - Examples for forex data sources
56
+
57
+ ### Features
58
+ - **Forex Data Support**: Complete foreign exchange data provider framework
59
+ - **Utility Functions**: Date and time utilities for data processing
60
+ - **Enhanced Wrappers**: Improved data provider wrapper capabilities
61
+ - **Example Code**: Forex data source examples and usage patterns
62
+
63
+ ### Documentation
64
+ - Added forex provider documentation
65
+ - Updated package exports to include new modules
66
+ - Enhanced examples with forex data sources
67
+
45
68
  ## [Unreleased]
46
69
 
47
70
  ### Planned
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-retrieval-module
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: A standardized interface for data providers with sync and async support
5
5
  Author-email: AbigailWilliams1692 <abigail.williams@example.com>
6
6
  Maintainer-email: AbigailWilliams1692 <abigail.williams@example.com>
@@ -28,6 +28,7 @@ Requires-Python: >=3.8
28
28
  Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: typing-extensions>=4.0.0; python_version < "3.10"
31
+ Requires-Dist: forex-python>=1.0.0
31
32
  Provides-Extra: dev
32
33
  Requires-Dist: pytest>=7.0.0; extra == "dev"
33
34
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -30,6 +30,13 @@ from data_retrieval.data_provider.rest_api import RestAPI_DataProvider
30
30
  # Database providers
31
31
  from data_retrieval.data_provider.database import Database_DataProvider
32
32
 
33
+ # Foreign Exchange providers
34
+ from data_retrieval.foreign_exchange import (
35
+ Forex_DataProvider_Base,
36
+ ForexPython_DataProvider,
37
+ Forex_DataProvider_Wrapper
38
+ )
39
+
33
40
  #######################################################################
34
41
  # Public API
35
42
  #######################################################################
@@ -51,11 +58,16 @@ __all__ = [
51
58
 
52
59
  # Database providers
53
60
  "Database_DataProvider",
61
+
62
+ # Foreign Exchange providers
63
+ "Forex_DataProvider_Base",
64
+ "ForexPython_DataProvider",
65
+ "Forex_DataProvider_Wrapper",
54
66
  ]
55
67
 
56
68
  #######################################################################
57
69
  # Version Information
58
70
  #######################################################################
59
- __version__ = "1.0.0"
71
+ __version__ = "1.0.1"
60
72
  __author__ = "AbigailWilliams1692"
61
73
  __email__ = "abigail.williams@example.com"
@@ -0,0 +1,3 @@
1
+ from data_retrieval.foreign_exchange.forex_data_provider_base import Forex_DataProvider_Base
2
+ from data_retrieval.foreign_exchange.forex_python_data_provider import ForexPython_DataProvider
3
+ from data_retrieval.foreign_exchange.forex_data_provider_wrapper import Forex_DataProvider_Wrapper
@@ -0,0 +1,140 @@
1
+ #######################################################################
2
+ # Project: Data Retrieval Module
3
+ # File: forex_data_provider_base.py
4
+ # Description: Forex Data Provider Base
5
+ # Author: AbigailWilliams1692
6
+ # Created: 2026-01-24
7
+ # Updated: 2026-01-24
8
+ #######################################################################
9
+
10
+ #######################################################################
11
+ # Import Packages
12
+ #######################################################################
13
+ # Standard Packages
14
+ import datetime
15
+ import logging
16
+ from abc import ABC, abstractmethod
17
+ from typing import (
18
+ Dict,
19
+ List,
20
+ Optional,
21
+ Union,
22
+ )
23
+
24
+ # Third-Party Packages
25
+
26
+ # Local Packages
27
+ from data_retrieval.model import DataProvider
28
+
29
+
30
+ #######################################################################
31
+ # Base Class
32
+ #######################################################################
33
+ class Forex_DataProvider_Base(DataProvider, ABC):
34
+ """
35
+ Forex Data Provider Base Class
36
+ """
37
+
38
+ #################################################
39
+ # Class Attributes
40
+ #################################################
41
+ __name: str = "Forex_DataProvider_Base"
42
+ __type: str = "DataProvider"
43
+
44
+ #################################################
45
+ # Constructor
46
+ #################################################
47
+ def __init__(
48
+ self,
49
+ instance_id: Optional[int] = None,
50
+ logger: Optional[logging.Logger] = None,
51
+ log_level: Optional[int] = logging.INFO,
52
+ **config,
53
+ ) -> None:
54
+ """
55
+ Initialize the data provider.
56
+
57
+ :param instance_id: Unique identifier for this provider instance.
58
+ :param logger: Logger instance for logging operations.
59
+ :param log_level: Logging level for the provider.
60
+ :param config: Additional configuration parameters.
61
+ """
62
+ # Initialize the base DataModule
63
+ super().__init__(
64
+ instance_id=instance_id,
65
+ logger=logger,
66
+ log_level=log_level,
67
+ **config
68
+ )
69
+
70
+ # Update the data methods
71
+ self.update_data_methods(
72
+ new_methods={
73
+ "exchange_rate_on_spot": self.get_exchange_rate_on_spot,
74
+ "exchange_rate_historical": self.get_exchange_rates_historical,
75
+ }
76
+ )
77
+
78
+ #################################################
79
+ # Connection Methods
80
+ #################################################
81
+ @abstractmethod
82
+ def _connect(self) -> None:
83
+ """
84
+ Connect to the data source.
85
+ """
86
+ raise NotImplementedError("Subclasses must implement this method")
87
+
88
+ @abstractmethod
89
+ def _disconnect(self) -> None:
90
+ """
91
+ Disconnect from the data source.
92
+ """
93
+ raise NotImplementedError("Subclasses must implement this method")
94
+
95
+ #################################################
96
+ # Core Instance Methods
97
+ #################################################
98
+ @abstractmethod
99
+ def get_exchange_rate_on_spot(self,
100
+ base_currency: str,
101
+ target_currency: str,
102
+ fx_datetime: Optional[Union[datetime.datetime, datetime.date]] = None,
103
+ *args,
104
+ **kwargs,
105
+ ) -> float:
106
+ """
107
+ Get the exchange rate for a given currency pair on the current date.
108
+
109
+ :param base_currency: The base currency code (e.g., 'USD').
110
+ :param target_currency: The target currency code (e.g., 'EUR').
111
+ :param fx_datetime: The timestamp for the exchange rate. (could be datetime or date)
112
+ :param args: Additional positional arguments.
113
+ :param kwargs: Additional keyword arguments.
114
+ :return: The exchange rate as a float.
115
+ """
116
+ raise NotImplementedError("Subclasses must implement this method")
117
+
118
+ @abstractmethod
119
+ def get_exchange_rates_historical(
120
+ self,
121
+ base_currency: str,
122
+ target_currencies: List[str],
123
+ fx_datetime_start: Union[datetime.datetime, datetime.date],
124
+ fx_datetime_end: Union[datetime.datetime, datetime.date],
125
+ *args,
126
+ **kwargs,
127
+ ) -> Dict[str, Dict[str, float]]:
128
+ """
129
+ Get the exchange rates for a base currency and multiple target currencies over a time range.
130
+
131
+ :param base_currency: The base currency code (e.g., 'USD').
132
+ :param target_currencies: The target currency codes (e.g., ['EUR', 'GBP']).
133
+ :param fx_datetime_start: The start timestamp for the exchange rate. (could be datetime or date)
134
+ :param fx_datetime_end: The end timestamp for the exchange rate. (could be datetime or date)
135
+ :param args: Additional positional arguments.
136
+ :param kwargs: Additional keyword arguments.
137
+ :return: A dictionary mapping dates to exchange rates.
138
+ """
139
+ raise NotImplementedError("Subclasses must implement this method")
140
+
@@ -0,0 +1,148 @@
1
+ #######################################################################
2
+ # Project: Data Retrieval Module
3
+ # File: forex_data_provider_wrapper.py
4
+ # Description: Forex Data Provider Wrapper
5
+ # Author: AbigailWilliams1692
6
+ # Created: 2026-01-24
7
+ # Updated: 2026-01-24
8
+ #######################################################################
9
+
10
+ #######################################################################
11
+ # Import Packages
12
+ #######################################################################
13
+ # Standard Packages
14
+ import datetime
15
+ import logging
16
+ from typing import (
17
+ Any,
18
+ Dict,
19
+ List,
20
+ Optional,
21
+ Union,
22
+ )
23
+
24
+ # Third-party Packages
25
+
26
+ # Local Packages
27
+ from data_retrieval.model import DataProvider_Wrapper
28
+ from data_retrieval.foreign_exchange.forex_data_provider_base import Forex_DataProvider_Base
29
+ from data_retrieval.foreign_exchange.forex_python_data_provider import ForexPython_DataProvider
30
+
31
+
32
+ #######################################################################
33
+ # Forex Data Provider (Synchronous)
34
+ #######################################################################
35
+ class Forex_DataProvider_Wrapper(DataProvider_Wrapper):
36
+ """
37
+ Foreign Exchange Data Provider Wrapper.
38
+
39
+ This class provides a standard wrapper interface for retrieving Foreign Exchange data from various sources.
40
+ """
41
+
42
+ #################################################
43
+ # Class Attributes
44
+ #################################################
45
+ __name: str = "Forex_DataProvider_Wrapper"
46
+ __type: str = "DataProvider_Wrapper"
47
+ __valid_data_sources = [
48
+ "forex-python",
49
+ "exchange_rate_api",
50
+ ]
51
+ __valid_data_source_mapping = {
52
+ "forex-python": ForexPython_DataProvider,
53
+ "exchange_rate_api": None,
54
+ }
55
+
56
+ #################################################
57
+ # Constructor
58
+ #################################################
59
+ def __init__(
60
+ self,
61
+ data_source: str = "forex-python",
62
+ instance_id: Optional[int] = None,
63
+ logger: Optional[logging.Logger] = None,
64
+ log_level: Optional[int] = logging.INFO,
65
+ **config,
66
+ ) -> None:
67
+ """
68
+ Initialize the Forex Data Provider.
69
+
70
+ :param instance_id: Unique identifier for this provider instance.
71
+ :param logger: Logger instance for logging operations.
72
+ :param log_level: Logging level for the provider.
73
+ :param data_methods: Dictionary of data retrieval methods.
74
+ :param config: Additional configuration parameters.
75
+ """
76
+ # Initialize the base DataProvider
77
+ super().__init__(
78
+ data_source=data_source,
79
+ instance_id=instance_id,
80
+ logger=logger,
81
+ log_level=log_level,
82
+ valid_data_sources=self.__valid_data_sources,
83
+ valid_data_source_mapping=self.__valid_data_source_mapping,
84
+ **config,
85
+ )
86
+
87
+ #################################################
88
+ # Core Instance Method: Get Exchange Rate
89
+ #################################################
90
+ def get_exchange_rate_on_spot(
91
+ self,
92
+ base_currency: str,
93
+ target_currency: str,
94
+ fx_datetime: Optional[Union[datetime.datetime, datetime.date]] = None,
95
+ *args,
96
+ **kwargs,
97
+ ) -> Any:
98
+ """
99
+ Get the exchange rate for a given currency pair on the current date.
100
+
101
+ :param base_currency: The base currency code (e.g., 'USD').
102
+ :param target_currency: The target currency code (e.g., 'EUR').
103
+ :param fx_datetime: The timestamp for the exchange rate. (could be datetime or date)
104
+ :param args: Additional positional arguments.
105
+ :param kwargs: Additional keyword arguments.
106
+ :return: The exchange rate as a float.
107
+ """
108
+ return self.fetch_data(
109
+ data_point="exchange_rate_on_spot",
110
+ return_data_type=float,
111
+ base_currency=base_currency,
112
+ target_currency=target_currency,
113
+ fx_datetime=fx_datetime,
114
+ *args,
115
+ **kwargs,
116
+ )
117
+
118
+ def get_exchange_rates_historical(
119
+ self,
120
+ base_currency: str,
121
+ target_currencies: List[str],
122
+ fx_datetime_start: Union[datetime.datetime, datetime.date],
123
+ fx_datetime_end: Union[datetime.datetime, datetime.date],
124
+ *args,
125
+ **kwargs,
126
+ ) -> Any:
127
+ """
128
+ Get the exchange rates for a base currency and multiple target currencies over a time range.
129
+
130
+ :param base_currency: The base currency code (e.g., 'USD').
131
+ :param target_currencies: The target currency codes (e.g., ['EUR', 'GBP']).
132
+ :param fx_datetime_start: The start timestamp for the exchange rate. (could be datetime or date)
133
+ :param fx_datetime_end: The end timestamp for the exchange rate. (could be datetime or date)
134
+ :param args: Additional positional arguments.
135
+ :param kwargs: Additional keyword arguments.
136
+ :return: The exchange rate as a float.
137
+ """
138
+ return self.fetch_data(
139
+ data_point="exchange_rate_historical",
140
+ return_data_type=Dict,
141
+ base_currency=base_currency,
142
+ target_currencies=target_currencies,
143
+ fx_datetime_start=fx_datetime_start,
144
+ fx_datetime_end=fx_datetime_end,
145
+ *args,
146
+ **kwargs,
147
+ )
148
+
@@ -0,0 +1,161 @@
1
+ ######################################################################
2
+ # Project: Data Retrieval Module
3
+ # File: forex_data_provider_base.py
4
+ # Description: Forex Data Provider Base
5
+ # Author: AbigailWilliams1692
6
+ # Created: 2026-01-24
7
+ # Updated: 2026-01-24
8
+ #######################################################################
9
+
10
+ #######################################################################
11
+ # Import Packages
12
+ #######################################################################
13
+ # Standard Packages
14
+ import datetime
15
+ import logging
16
+ from typing import (
17
+ Dict,
18
+ List,
19
+ Optional,
20
+ Union,
21
+ )
22
+
23
+ # Third-Party Packages
24
+ from forex_python.converter import CurrencyCodes
25
+ from forex_python.converter import CurrencyRates
26
+ from forex_python.bitcoin import BtcConverter
27
+
28
+ # Local Packages
29
+ from data_retrieval.foreign_exchange.forex_data_provider_base import Forex_DataProvider_Base
30
+ from data_retrieval.utils.date_utils import populate_dates_in_between
31
+
32
+
33
+ #######################################################################
34
+ # Forex Python Data Provider Class
35
+ #######################################################################
36
+ class ForexPython_DataProvider(Forex_DataProvider_Base):
37
+ """
38
+ Forex Python Data Provider Class
39
+ """
40
+
41
+ #################################################
42
+ # Class Attributes
43
+ #################################################
44
+ __name: str = "ForexPython_DataProvider"
45
+ __type: str = "DataProvider"
46
+
47
+ #################################################
48
+ # Constructor
49
+ #################################################
50
+ def __init__(
51
+ self,
52
+ instance_id: Optional[int] = None,
53
+ logger: Optional[logging.Logger] = None,
54
+ log_level: Optional[int] = logging.INFO,
55
+ **config,
56
+ ) -> None:
57
+ """
58
+ Initialize the data provider.
59
+
60
+ :param instance_id: Unique identifier for this provider instance.
61
+ :param logger: Logger instance for logging operations.
62
+ :param log_level: Logging level for the provider.
63
+ :param config: Additional configuration parameters.
64
+ """
65
+ # Initialize the base DataModule
66
+ super().__init__(
67
+ instance_id=instance_id,
68
+ logger=logger,
69
+ log_level=log_level,
70
+ **config
71
+ )
72
+
73
+ # Connect to the data source
74
+ self.connect()
75
+
76
+ #################################################
77
+ # Connection Methods
78
+ #################################################
79
+ def _connect(self) -> None:
80
+ """
81
+ Connect to the data source.
82
+ """
83
+ self.set_connection(
84
+ {
85
+ "currency_codes": CurrencyCodes(),
86
+ "currency_rates": CurrencyRates(),
87
+ "btc_converter": BtcConverter(),
88
+ }
89
+ )
90
+
91
+ def _disconnect(self) -> None:
92
+ """
93
+ Disconnect from the data source.
94
+ """
95
+ self.set_connection(None)
96
+
97
+ #################################################
98
+ # Core Instance Methods
99
+ #################################################
100
+ def get_exchange_rate_on_spot(
101
+ self,
102
+ base_currency: str,
103
+ target_currency: str,
104
+ fx_datetime: Optional[Union[datetime.datetime, datetime.date]] = None,
105
+ *args,
106
+ **kwargs,
107
+ ) -> float:
108
+ """
109
+ Get the exchange rate for a given currency pair on the current date.
110
+
111
+ :param base_currency: The base currency code (e.g., 'USD').
112
+ :param target_currency: The target currency code (e.g., 'EUR').
113
+ :param fx_datetime: The timestamp for the exchange rate. (could be datetime or date)
114
+ :param args: Additional positional arguments.
115
+ :param kwargs: Additional keyword arguments.
116
+ :return: The exchange rate as a float.
117
+ """
118
+ # Get the connection
119
+ currency_rates = self.get_connection().get("currency_rates")
120
+
121
+ # Get the exchange rate
122
+ return currency_rates.get_rate(base_cur=base_currency, dest_cur=target_currency, date_obj=fx_datetime)
123
+
124
+ def get_exchange_rates_historical(
125
+ self,
126
+ base_currency: str,
127
+ target_currencies: List[str],
128
+ fx_datetime_start: Union[datetime.datetime, datetime.date],
129
+ fx_datetime_end: Union[datetime.datetime, datetime.date],
130
+ *args,
131
+ **kwargs,
132
+ ) -> Dict[datetime.date, Dict[str, Optional[float]]]:
133
+ """
134
+ Get the exchange rates for a given currency pair on the current date.
135
+
136
+ :param base_currency: The base currency code (e.g., 'USD').
137
+ :param target_currencies: The target currency codes (e.g., ['EUR', 'GBP']).
138
+ :param fx_datetime_start: The start timestamp for the exchange rate. (could be datetime or date)
139
+ :param fx_datetime_end: The end timestamp for the exchange rate. (could be datetime or date)
140
+ :param args: Additional positional arguments.
141
+ :param kwargs: Additional keyword arguments.
142
+ :return: The exchange rates as a dictionary.
143
+ """
144
+ # Initialize the exchange rates container
145
+ exchange_rates = {}
146
+
147
+ # Get the connection
148
+ currency_rates = self.get_connection().get("currency_rates")
149
+
150
+ # Iterate through the date list and get the exchange rates for each date
151
+ date_list = populate_dates_in_between(start_date=fx_datetime_start, end_date=fx_datetime_end, interval=1)
152
+ for date in date_list:
153
+ raw_record = currency_rates.get_rates(base_cur=base_currency, date_obj=date)
154
+ exchange_rate_record = {
155
+ f"{base_currency}/{target_currency}": raw_record.get(target_currency, None)
156
+ for target_currency in target_currencies
157
+ }
158
+ exchange_rates[date] = exchange_rate_record
159
+
160
+ return exchange_rates
161
+
@@ -4,8 +4,9 @@
4
4
  # Description: Model Package Initialization
5
5
  # Author: AbigailWilliams1692
6
6
  # Created: 2026-01-14
7
- # Updated: 2026-01-18
7
+ # Updated: 2026-01-24
8
8
  #######################################################################
9
9
 
10
10
  from data_retrieval.model.data_module import DataModule
11
11
  from data_retrieval.model.data_provider import DataProvider
12
+ from data_retrieval.model.data_provider_wrapper import DataProvider_Wrapper
@@ -77,7 +77,6 @@ class DataProvider(DataModule):
77
77
  :param instance_id: Unique identifier for this provider instance.
78
78
  :param logger: Logger instance for logging operations.
79
79
  :param log_level: Logging level for the provider.
80
- :param data_methods: Dictionary of data retrieval methods.
81
80
  :param config: Additional configuration parameters.
82
81
  """
83
82
  # Initialize the base DataModule
@@ -0,0 +1,428 @@
1
+ #######################################################################
2
+ # Project: Data Retrieval Module
3
+ # File: data_provider_wrapper.py
4
+ # Description: Wrapper class for managing multiple data providers
5
+ # Author: AbigailWilliams1692
6
+ # Created: 2026-01-24
7
+ # Updated: 2026-01-24
8
+ #######################################################################
9
+
10
+ #######################################################################
11
+ # Import Packages
12
+ #######################################################################
13
+ # Standard Packages
14
+ import logging
15
+ from typing import (
16
+ Any,
17
+ Optional,
18
+ Dict,
19
+ List,
20
+ Type,
21
+ )
22
+
23
+ # Local Packages
24
+ from data_retrieval.model.data_module import DataModule
25
+ from data_retrieval.model.data_provider import DataProvider
26
+ from data_retrieval.model.exceptions import (
27
+ DataProviderError,
28
+ DataProviderNotFoundError,
29
+ DataProviderInitializationError,
30
+ DataProviderWrapperConnectionError,
31
+ )
32
+
33
+
34
+ #######################################################################
35
+ # Data Provider Wrapper Connection Status
36
+ #######################################################################
37
+ class DataProviderWrapperConnectionStatus:
38
+ """
39
+ Connection status for DataProvider_Wrapper.
40
+ """
41
+ CONNECTED = "connected"
42
+ DISCONNECTED = "disconnected"
43
+
44
+
45
+ #######################################################################
46
+ # Data Provider Wrapper Class
47
+ #######################################################################
48
+ class DataProvider_Wrapper(DataModule):
49
+ """
50
+ Wrapper class for managing multiple data providers with a unified interface.
51
+
52
+ This class allows switching between different data providers (data sources)
53
+ for fetching the same category of data. It mimics the DataProvider interface
54
+ while providing the flexibility to choose which underlying provider to use.
55
+ """
56
+
57
+ #################################################
58
+ # Class Attributes
59
+ #################################################
60
+ __name: str = "DataProvider_Wrapper"
61
+ __type: str = "DataProvider_Wrapper"
62
+
63
+ #################################################
64
+ # Constructor
65
+ #################################################
66
+ def __init__(
67
+ self,
68
+ data_source: str,
69
+ instance_id: Optional[int] = None,
70
+ logger: Optional[logging.Logger] = None,
71
+ log_level: Optional[int] = logging.INFO,
72
+ valid_data_sources: Optional[List[str]] = None,
73
+ valid_data_source_mapping: Optional[Dict[str, Type[DataProvider]]] = None,
74
+ **config,
75
+ ) -> None:
76
+ """
77
+ Initialize the data provider wrapper.
78
+
79
+ :param data_source: The data source name to use for fetching data.
80
+ :param instance_id: Unique identifier for this wrapper instance.
81
+ :param logger: Logger instance for logging operations.
82
+ :param log_level: Logging level for the wrapper.
83
+ :param config: Additional configuration parameters.
84
+ """
85
+ # Initialize the base DataModule
86
+ super().__init__(
87
+ instance_id=instance_id,
88
+ logger=logger,
89
+ log_level=log_level,
90
+ )
91
+
92
+ # Initialize DataProvider_Wrapper attributes
93
+ ## Valid data sources and mappings
94
+ self.__valid_data_sources = valid_data_sources
95
+ self.__valid_data_source_mapping = valid_data_source_mapping
96
+
97
+ ## Other attributes
98
+ self._config = config or {}
99
+ self._data_source: Optional[str] = data_source
100
+ self._data_provider: Optional[DataProvider] = self._initialize_data_provider_instance(data_source=data_source)
101
+
102
+ # Set initial status for the DataProvider_Wrapper instance
103
+ self.set_status(DataProviderWrapperConnectionStatus.DISCONNECTED)
104
+
105
+ #################################################
106
+ # Getter & Setter Methods
107
+ #################################################
108
+ def get_valid_data_sources(self) -> List[str]:
109
+ """
110
+ Get the list of valid data sources.
111
+
112
+ :return: The list of valid data sources.
113
+ """
114
+ return self.__valid_data_sources
115
+
116
+ def set_valid_data_sources(self, valid_data_sources: List[str]) -> None:
117
+ """
118
+ Set the list of valid data sources.
119
+
120
+ :param valid_data_sources: The list of valid data sources to set.
121
+ :return: None.
122
+ """
123
+ self.__valid_data_sources = valid_data_sources
124
+
125
+ def add_valid_data_source(self, valid_data_source: str) -> None:
126
+ """
127
+ Add a valid data source to the list of valid data sources.
128
+
129
+ :param valid_data_source: The valid data source to add.
130
+ :return: None.
131
+ """
132
+ self.__valid_data_sources.append(valid_data_source)
133
+
134
+ def remove_valid_data_source(self, valid_data_source: str) -> None:
135
+ """
136
+ Remove a valid data source from the list of valid data sources.
137
+
138
+ :param valid_data_source: The valid data source to remove.
139
+ :return: None.
140
+ """
141
+ self.__valid_data_sources.remove(valid_data_source)
142
+
143
+ def get_valid_data_source_mapping(self) -> Dict[str, Type[DataProvider]]:
144
+ """
145
+ Get the mapping of valid data sources to their corresponding DataProvider classes.
146
+
147
+ :return: The mapping of valid data sources to their corresponding DataProvider classes.
148
+ """
149
+ return self.__valid_data_source_mapping
150
+
151
+ def set_valid_data_source_mapping(self, valid_data_source_mapping: Dict[str, Type[DataProvider]]) -> None:
152
+ """
153
+ Set the mapping of valid data sources to their corresponding DataProvider classes.
154
+
155
+ :param valid_data_source_mapping: The mapping of valid data sources to their corresponding DataProvider classes.
156
+ :return: None.
157
+ """
158
+ self.__valid_data_source_mapping = valid_data_source_mapping
159
+
160
+ def update_valid_data_source_mapping(self, valid_data_source: str, data_provider_class: Type[DataProvider]) -> None:
161
+ """
162
+ Update the mapping of a valid data source to its corresponding DataProvider class.
163
+
164
+ :param valid_data_source: The valid data source to update.
165
+ :param data_provider_class: The DataProvider class to map to the valid data source.
166
+ :return: None.
167
+ """
168
+ self.__valid_data_source_mapping[valid_data_source] = data_provider_class
169
+
170
+ def get_config(self) -> Dict[str, Any]:
171
+ """
172
+ Get the configuration dictionary.
173
+
174
+ :return: The configuration dictionary.
175
+ """
176
+ return self._config
177
+
178
+ def set_config(self, config: Dict[str, Any]) -> None:
179
+ """
180
+ Set the configuration dictionary.
181
+
182
+ :param config: The configuration dictionary.
183
+ :return: None.
184
+ """
185
+ self._config = config
186
+
187
+ def update_config(self, key: str, value: Any) -> None:
188
+ """
189
+ Update a specific configuration key-value pair.
190
+
191
+ :param key: The configuration key to update.
192
+ :param value: The new value for the configuration key.
193
+ :return: None.
194
+ """
195
+ self._config.update({key: value})
196
+
197
+ def get_data_source(self) -> Optional[str]:
198
+ """
199
+ Get the current data source.
200
+
201
+ :return: The current data source name.
202
+ """
203
+ return self._data_source
204
+
205
+ def set_data_source(self, data_source: str) -> None:
206
+ """
207
+ Validate the data source first and set the data source to use for fetching data.
208
+
209
+ :param data_source: The name of the data source to use.
210
+ :return: None.
211
+ :raises KeyError: If the data source is not registered.
212
+ """
213
+ if not self.validate_data_source(data_source):
214
+ raise KeyError(f"Data source '{data_source}' is not valid.")
215
+ self._data_source = data_source
216
+
217
+ def get_data_provider(self) -> Optional[DataProvider]:
218
+ """
219
+ Get the current data provider.
220
+
221
+ :return: The current data provider instance.
222
+ """
223
+ return self._data_provider
224
+
225
+ def set_data_provider(self, data_provider: DataProvider) -> None:
226
+ """
227
+ Set the current data provider.
228
+
229
+ :param data_provider: The DataProvider instance to set.
230
+ :return: None.
231
+ """
232
+ self._data_provider = data_provider
233
+
234
+ #################################################
235
+ # Data Provider Management Methods
236
+ #################################################
237
+ def switch_data_provider(self, data_source: str) -> None:
238
+ """
239
+ Switch to another data provider that has already been registered.
240
+
241
+ :param data_source: The data source name to register.
242
+ :return: None.
243
+ """
244
+ if data_source == self.get_data_source():
245
+ raise ValueError(f"Data source '{data_source}' is already the current data source.")
246
+ elif self.validate_data_source(data_source=data_source):
247
+ self.set_data_source(data_source=data_source)
248
+ self.set_data_provider(
249
+ data_provider=self._initialize_data_provider_instance(data_source=data_source)
250
+ )
251
+
252
+ def register_data_provider(self, data_source: str, data_provider_class: Type[DataProvider]) -> None:
253
+ """
254
+ Register a new data provider class.
255
+
256
+ :param data_source: The data source name to register.
257
+ :param data_provider_class: The DataProvider class to register.
258
+ :return: None.
259
+ """
260
+ self.__valid_data_sources[data_source] = data_provider_class
261
+
262
+ def unregister_data_provider(self, data_source: str) -> None:
263
+ """
264
+ Unregister a data provider class.
265
+
266
+ :param data_source: The data source name to unregister.
267
+ :return: None.
268
+ """
269
+ if data_source in self.__valid_data_sources:
270
+ del self.__valid_data_sources[data_source]
271
+
272
+ def _initialize_data_provider_instance(self, data_source: Optional[str] = None) -> DataProvider:
273
+ """
274
+ Initialize the data provider as the data source instance.
275
+
276
+ :param data_source: The data source to use. If None, uses the current data source.
277
+ :return: The initialized data provider instance.
278
+ """
279
+ # Check if data source is None.
280
+ if data_source is None:
281
+ data_source = self.get_data_source()
282
+
283
+ # Get the data provider class from the mapping
284
+ data_provider_class = self.__valid_data_source_mapping.get(data_source, None)
285
+
286
+ # Check if the data provider class is found
287
+ if data_provider_class is None:
288
+ raise DataProviderNotFoundError(
289
+ f"No data provider found for data source: '{data_source}'."
290
+ )
291
+
292
+ # Initialize the data provider instance
293
+ try:
294
+ data_provider_instance = data_provider_class(
295
+ log_level=self.get_log_level(),
296
+ **self.get_config(),
297
+ )
298
+ except DataProviderError as e:
299
+ raise e
300
+ except Exception as e:
301
+ raise DataProviderInitializationError(
302
+ f"Failed to initialize data provider for data source: '{data_source}'. Error: {str(e)}"
303
+ )
304
+
305
+ return data_provider_instance
306
+
307
+ #################################################
308
+ # Connection Methods
309
+ #################################################
310
+ def is_connected(self) -> bool:
311
+ """
312
+ Check if the specified data provider is connected.
313
+
314
+ :return: True if connected, False otherwise.
315
+ """
316
+ data_provider = self.get_data_provider()
317
+ if data_provider is None:
318
+ raise DataProviderNotFoundError(
319
+ f"No data provider found for data source: '{self.get_data_source()}'."
320
+ )
321
+ return data_provider.is_connected()
322
+
323
+ def connect(self) -> None:
324
+ """
325
+ Connect to the specified data provider.
326
+ """
327
+ try:
328
+ data_provider = self.get_data_provider()
329
+ if data_provider is None:
330
+ raise DataProviderNotFoundError(
331
+ f"No data provider found for data source: '{self.get_data_source()}'."
332
+ )
333
+ data_provider.connect()
334
+ self.set_status(DataProviderWrapperConnectionStatus.CONNECTED)
335
+ except DataProviderError as e:
336
+ raise e
337
+ except Exception as e:
338
+ raise DataProviderWrapperConnectionError(
339
+ f"Failed to connect to data provider for data source: '{self.get_data_source()}'. Error: {str(e)}"
340
+ )
341
+
342
+ def disconnect(self) -> None:
343
+ """
344
+ Disconnect from the specified data provider.
345
+ """
346
+ try:
347
+ data_provider = self.get_data_provider()
348
+ if data_provider is None:
349
+ raise DataProviderNotFoundError(
350
+ f"No data provider found for data source: '{self.get_data_source()}'."
351
+ )
352
+ data_provider.disconnect()
353
+ self.set_status(DataProviderWrapperConnectionStatus.DISCONNECTED)
354
+ except DataProviderError as e:
355
+ raise e
356
+ except Exception as e:
357
+ raise DataProviderWrapperConnectionError(
358
+ f"Failed to disconnect from data provider for data source: '{self.get_data_source()}'. Error: {str(e)}"
359
+ )
360
+
361
+ #################################################
362
+ # Context Management
363
+ #################################################
364
+ def __enter__(self) -> "DataProvider":
365
+ """
366
+ Enter the runtime context related to this object. Automatically connects to the data source.
367
+ """
368
+ return self
369
+
370
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
371
+ """
372
+ Exit the runtime context related to this object. Automatically disconnects from the data source.
373
+ """
374
+ self.disconnect()
375
+
376
+ #################################################
377
+ # Core Instance Method: Fetch Data
378
+ #################################################
379
+ def fetch_data(
380
+ self,
381
+ data_point: str,
382
+ return_data_type: type,
383
+ *args,
384
+ **kwargs,
385
+ ) -> Any:
386
+ """
387
+ Fetch data from the specified data source.
388
+
389
+ :param data_point: The data point to fetch.
390
+ :param return_data_type: The expected return data type.
391
+ :param args: Positional arguments for fetching data.
392
+ :param kwargs: Keyword arguments for fetching data.
393
+ :return: The fetched data.
394
+ :raises DataProviderNotFoundError: If no provider is found for the data source.
395
+ :raises DataMethodNotFoundError: If the data method is not found.
396
+ :raises ReturnDataTypeNotMatchedError: If the return data type does not match.
397
+ """
398
+ # Get the data provider for the current data source
399
+ data_provider = self.get_data_provider()
400
+
401
+ # Check if the data provider exists
402
+ if data_provider is None:
403
+ raise DataProviderNotFoundError(
404
+ f"No data provider found for data source '{self.get_data_source()}'."
405
+ )
406
+
407
+ # Fetch the data
408
+ data: Any = data_provider.fetch_data(
409
+ data_point=data_point,
410
+ return_data_type=return_data_type,
411
+ *args,
412
+ **kwargs
413
+ )
414
+
415
+ return data
416
+
417
+ #################################################
418
+ # Utility Methods
419
+ #################################################
420
+ def validate_data_source(self, data_source: str) -> bool:
421
+ """
422
+ Validate if the provided data source is supported.
423
+
424
+ :param data_source: The data source to validate.
425
+ :return: True if the data source is valid, False otherwise.
426
+ """
427
+ print(self.get_valid_data_sources())
428
+ return data_source in self.__valid_data_sources
@@ -4,17 +4,22 @@
4
4
  # Description: Exception classes for data providers
5
5
  # Author: AbigailWilliams1692
6
6
  # Created: 2026-01-14
7
- # Updated: 2026-01-19
7
+ # Updated: 2026-01-24
8
8
  #######################################################################
9
9
 
10
10
  #######################################################################
11
- # Exception Classes
11
+ # Data Provider Exceptions
12
12
  #######################################################################
13
13
  class DataProviderError(Exception):
14
14
  """Base exception for data provider errors."""
15
15
  pass
16
16
 
17
17
 
18
+ class DataProviderInitializationError(DataProviderError):
19
+ """Raised when initialization of data provider fails."""
20
+ pass
21
+
22
+
18
23
  class DataProviderConnectionError(DataProviderError):
19
24
  """Raised when connection to data source fails."""
20
25
  pass
@@ -36,3 +41,19 @@ class ReturnDataTypeNotMatchedError(DataProviderError):
36
41
  class ValidationError(DataProviderError):
37
42
  """Raised when data validation fails."""
38
43
  pass
44
+
45
+
46
+ #######################################################################
47
+ # Data Provider Wrapper Exceptions
48
+ #######################################################################
49
+ class DataProviderWrapperError(Exception):
50
+ """Base exception for data provider wrapper errors."""
51
+ pass
52
+
53
+ class DataProviderNotFoundError(DataProviderWrapperError):
54
+ """Raised when cannot find the corresponding data provider class."""
55
+ pass
56
+
57
+ class DataProviderWrapperConnectionError(DataProviderWrapperError):
58
+ """Raised when connection to data provider fails."""
59
+ pass
@@ -0,0 +1,54 @@
1
+ #######################################################################
2
+ # Project: Data Retrieval Module
3
+ # File: date_utils.py
4
+ # Description: Util functions for date operations
5
+ # Author: AbigailWilliams1692
6
+ # Created: 2026-01-24
7
+ # Updated: 2026-01-24
8
+ #######################################################################
9
+
10
+ #######################################################################
11
+ # Import Packages
12
+ #######################################################################
13
+ # Standard Packages
14
+ import datetime
15
+ from typing import List, Tuple
16
+
17
+
18
+ #######################################################################
19
+ # Util Functions
20
+ #######################################################################
21
+ def populate_dates_in_between(
22
+ start_date: datetime.date,
23
+ end_date: datetime.date,
24
+ interval: int = 1
25
+ ) -> List[datetime.date]:
26
+ """
27
+ Populate a list of dates between start_date and end_date (inclusive).
28
+
29
+ :param start_date: The start date.
30
+ :param end_date: The end date.
31
+ :param interval: The interval between dates in days.
32
+ :return: List of dates between start_date and end_date.
33
+ """
34
+ if start_date > end_date:
35
+ raise ValueError("start_date must be before or equal to end_date")
36
+ delta = end_date - start_date
37
+ return [start_date + datetime.timedelta(days=i) for i in range(0, delta.days + 1, interval)]
38
+
39
+
40
+ def populate_start_and_end_dates(
41
+ start_date: datetime.date,
42
+ end_date: datetime.date,
43
+ interval: int = 1
44
+ ) -> List[Tuple[datetime.date, datetime.date]]:
45
+ """
46
+ Populate a list of dates between start_date and end_date (inclusive).
47
+
48
+ :param start_date: The start date.
49
+ :param end_date: The end date.
50
+ :param interval: The interval between dates in days.
51
+ :return: List of start date and end date tuples.
52
+ """
53
+ date_list = populate_dates_in_between(start_date=start_date, end_date=end_date, interval=interval)
54
+ return [(date_list[i], date_list[i+1]) for i in range(0, len(date_list)-1)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-retrieval-module
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: A standardized interface for data providers with sync and async support
5
5
  Author-email: AbigailWilliams1692 <abigail.williams@example.com>
6
6
  Maintainer-email: AbigailWilliams1692 <abigail.williams@example.com>
@@ -28,6 +28,7 @@ Requires-Python: >=3.8
28
28
  Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: typing-extensions>=4.0.0; python_version < "3.10"
31
+ Requires-Dist: forex-python>=1.0.0
31
32
  Provides-Extra: dev
32
33
  Requires-Dist: pytest>=7.0.0; extra == "dev"
33
34
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -8,10 +8,17 @@ requirements-test.txt
8
8
  setup.py
9
9
  data_retrieval/__init__.py
10
10
  data_retrieval/py.typed
11
+ data_retrieval/foreign_exchange/__init__.py
12
+ data_retrieval/foreign_exchange/forex_data_provider_base.py
13
+ data_retrieval/foreign_exchange/forex_data_provider_wrapper.py
14
+ data_retrieval/foreign_exchange/forex_python_data_provider.py
11
15
  data_retrieval/model/__init__.py
12
16
  data_retrieval/model/data_module.py
13
17
  data_retrieval/model/data_provider.py
18
+ data_retrieval/model/data_provider_wrapper.py
14
19
  data_retrieval/model/exceptions.py
20
+ data_retrieval/utils/__init__.py
21
+ data_retrieval/utils/date_utils.py
15
22
  data_retrieval_module.egg-info/PKG-INFO
16
23
  data_retrieval_module.egg-info/SOURCES.txt
17
24
  data_retrieval_module.egg-info/dependency_links.txt
@@ -1,3 +1,4 @@
1
+ forex-python>=1.0.0
1
2
 
2
3
  [:python_version < "3.10"]
3
4
  typing-extensions>=4.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "data-retrieval-module"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "A standardized interface for data providers with sync and async support"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -33,6 +33,7 @@ classifiers = [
33
33
  requires-python = ">=3.8"
34
34
  dependencies = [
35
35
  "typing-extensions>=4.0.0; python_version<'3.10'",
36
+ "forex-python>=1.0.0",
36
37
  ]
37
38
 
38
39
  [project.optional-dependencies]
@@ -79,7 +80,7 @@ Repository = "https://github.com/AbigailWilliams1692/data-retrieval-module"
79
80
  Changelog = "https://github.com/AbigailWilliams1692/data-retrieval-module/blob/main/CHANGELOG.md"
80
81
 
81
82
  [tool.setuptools]
82
- packages = ["data_retrieval", "data_retrieval.model"]
83
+ packages = ["data_retrieval", "data_retrieval.model", "data_retrieval.foreign_exchange", "data_retrieval.utils"]
83
84
 
84
85
  [tool.setuptools.package-data]
85
86
  data_retrieval = ["py.typed"]
@@ -26,7 +26,7 @@ def test_version():
26
26
  import data_retrieval
27
27
 
28
28
  assert hasattr(data_retrieval, '__version__')
29
- assert data_retrieval.__version__ == "1.0.0"
29
+ assert data_retrieval.__version__ == "1.0.1"
30
30
 
31
31
  # Test package structure
32
32
  def test_package_structure():