forexrates 0.0.1.dev0__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.
- forexrates-0.0.1.dev0/LICENSE +21 -0
- forexrates-0.0.1.dev0/PKG-INFO +40 -0
- forexrates-0.0.1.dev0/forexrates/__init__.py +16 -0
- forexrates-0.0.1.dev0/forexrates/api/__init__.py +1 -0
- forexrates-0.0.1.dev0/forexrates/api/base.py +142 -0
- forexrates-0.0.1.dev0/forexrates/api/exchangerates.py +65 -0
- forexrates-0.0.1.dev0/forexrates/io/__init__.py +10 -0
- forexrates-0.0.1.dev0/forexrates/io/base.py +30 -0
- forexrates-0.0.1.dev0/forexrates/io/dataframe.py +137 -0
- forexrates-0.0.1.dev0/forexrates.egg-info/PKG-INFO +40 -0
- forexrates-0.0.1.dev0/forexrates.egg-info/SOURCES.txt +17 -0
- forexrates-0.0.1.dev0/forexrates.egg-info/dependency_links.txt +1 -0
- forexrates-0.0.1.dev0/forexrates.egg-info/requires.txt +7 -0
- forexrates-0.0.1.dev0/forexrates.egg-info/top_level.txt +4 -0
- forexrates-0.0.1.dev0/main/config.py +49 -0
- forexrates-0.0.1.dev0/main/erapi.py +96 -0
- forexrates-0.0.1.dev0/pyproject.toml +68 -0
- forexrates-0.0.1.dev0/setup.cfg +4 -0
- forexrates-0.0.1.dev0/setup.py +69 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Debmalya Pramanik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forexrates
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: A unified codebase for fetching FOREX rates.
|
|
5
|
+
Home-page: https://github.com/sharkutilities/forexrates
|
|
6
|
+
Author: shark-utilities developers
|
|
7
|
+
Author-email: shark-utilities developers <neuralNOD@outlook.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
Project-URL: Homepage, https://github.com/sharkutilities/forexrates
|
|
10
|
+
Project-URL: Issue Tracker, https://github.com/sharkutilities/forexrates/issues
|
|
11
|
+
Project-URL: Org. Homepage, https://github.com/sharkutilities
|
|
12
|
+
Keywords: forex,api,exchange rates,foreign exchange rates,wrappers,data science,data analysis,data scientist,data analyst
|
|
13
|
+
Classifier: Development Status :: 1 - Planning
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: Operating System :: Unix
|
|
20
|
+
Classifier: Operating System :: POSIX
|
|
21
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
22
|
+
Classifier: Programming Language :: Python
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: numpy==2.4.2
|
|
31
|
+
Requires-Dist: pandas==3.0.0
|
|
32
|
+
Requires-Dist: psycopg==3.3.2
|
|
33
|
+
Requires-Dist: psycopg-binary==3.3.2
|
|
34
|
+
Requires-Dist: requests==2.32.5
|
|
35
|
+
Requires-Dist: SQLAlchemy==2.0.0
|
|
36
|
+
Requires-Dist: tqdm==4.67.3
|
|
37
|
+
Dynamic: author
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
A Python Package to Fetch FOREX Rates from API Sources
|
|
5
|
+
|
|
6
|
+
An one place that integrates different paid/open source API sources
|
|
7
|
+
to fetch foreign exchange rates.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# ? package follows https://peps.python.org/pep-0440/
|
|
11
|
+
# ? https://python-semver.readthedocs.io/en/latest/advanced/convert-pypi-to-semver.html
|
|
12
|
+
__version__ = "v0.0.1.dev0"
|
|
13
|
+
|
|
14
|
+
# init-time options registrations
|
|
15
|
+
from forexrates import io
|
|
16
|
+
from forexrates import api
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from forexrates.api.exchangerates import ExchangeRatesAPI
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Base API Class Wrapper and Structure for API Integration
|
|
5
|
+
|
|
6
|
+
The base class is designed to be uniform for all integrated external
|
|
7
|
+
data sources with abstract class methods and properties.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import requests
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
|
|
17
|
+
# GH#2 disable SSL warnings for self-signed certificates
|
|
18
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
19
|
+
|
|
20
|
+
class BaseAPI(ABC):
|
|
21
|
+
def __init__(self, apikey : str) -> None:
|
|
22
|
+
self.apikey = apikey
|
|
23
|
+
|
|
24
|
+
# create request sessions object for further use
|
|
25
|
+
self._session = requests.Session()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def uri(self) -> str:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def headers(self) -> dict:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def endpoint(self) -> str:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def params(self) -> dict:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def __format_error__(self, e : requests.exceptions.RequestException) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Format the Base Error Response Structure to Hide Sensitive Data
|
|
55
|
+
|
|
56
|
+
The make request exposes some sensitive information in the URL
|
|
57
|
+
string, which is not desired for security reasons. Referring
|
|
58
|
+
to an existing issue, the problem is not yet resolved and may
|
|
59
|
+
require an alternate approach.
|
|
60
|
+
|
|
61
|
+
Override the method in the child class to preserve the sensitive
|
|
62
|
+
information from leaking, or leave it to the parent class to
|
|
63
|
+
return the value as is.
|
|
64
|
+
|
|
65
|
+
:type e: requests.exceptions.RequestException
|
|
66
|
+
:param e: Exception to be formatted, to hide sensitive
|
|
67
|
+
information from the URL string.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
return e
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_request(self, method : str, **kwargs) -> requests.Response:
|
|
74
|
+
sleep = kwargs.get("sleep", 2) # sleep b/w requests, in seconds
|
|
75
|
+
retries = kwargs.get("retries", 3) # no. of retry attempts
|
|
76
|
+
|
|
77
|
+
# GH#2 give user the ability to disable SSL verification
|
|
78
|
+
# ? this is not recommended, but can be useful for testing
|
|
79
|
+
verify = kwargs.get("verify", True) # verify SSL certificate
|
|
80
|
+
|
|
81
|
+
if _ := kwargs.get("suppresswarning", False):
|
|
82
|
+
warnings.filterwarnings(
|
|
83
|
+
"ignore", category = InsecureRequestWarning
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
requests.packages.urllib3.disable_warnings(
|
|
87
|
+
InsecureRequestWarning
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# create uri and append the endpoint to the url::
|
|
91
|
+
uri = self.uri + self.endpoint
|
|
92
|
+
|
|
93
|
+
# create url load like headers and params
|
|
94
|
+
dataload = {"headers" : self.headers, "params" : self.params}
|
|
95
|
+
|
|
96
|
+
# retry on the uri with header and param loads
|
|
97
|
+
for attempt in range(retries):
|
|
98
|
+
try:
|
|
99
|
+
response = self._session.request(
|
|
100
|
+
method, uri, **dataload, verify = verify
|
|
101
|
+
)
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
|
|
104
|
+
return response # data received, return response
|
|
105
|
+
|
|
106
|
+
except requests.exceptions.RequestException as e:
|
|
107
|
+
e = self.__format_error__(e)
|
|
108
|
+
print(f"Request Failed (Attempt: {attempt + 1}) : {e}")
|
|
109
|
+
|
|
110
|
+
if attempt == retries - 1:
|
|
111
|
+
print("Max Retries Reached.")
|
|
112
|
+
raise e
|
|
113
|
+
|
|
114
|
+
time.sleep(sleep)
|
|
115
|
+
continue # continue until retries are exhausted
|
|
116
|
+
|
|
117
|
+
except requests.exceptions.Timeout as e:
|
|
118
|
+
print(f"Request Timed Out (Attempt: {attempt + 1}) : {e}")
|
|
119
|
+
|
|
120
|
+
if attempt == retries - 1:
|
|
121
|
+
print("Max Retries Reached.")
|
|
122
|
+
raise e
|
|
123
|
+
|
|
124
|
+
time.sleep(sleep)
|
|
125
|
+
continue # continue until retries are exhausted
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get(self, **kwargs) -> Iterable:
|
|
129
|
+
"""
|
|
130
|
+
Get the Base Response from an External API Source
|
|
131
|
+
|
|
132
|
+
The base response is fetched and the data is returned as
|
|
133
|
+
a JSON/Dictionary object to the enduser. The response can be
|
|
134
|
+
further processed and formatted into a desired structure using
|
|
135
|
+
the IO module.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return self.make_request("GET", **kwargs).json()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
self._session.close()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
API Handling for the Exchangerates API (https://exchangeratesapi.io/)
|
|
5
|
+
|
|
6
|
+
Exchange Rates API provides historic and real-time exchange rates and
|
|
7
|
+
currency conversions. More information about the API is available at
|
|
8
|
+
https://exchangeratesapi.io/documentation/.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
import datetime as dt
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
from forexrates.api.base import BaseAPI
|
|
16
|
+
|
|
17
|
+
class ExchangeRatesAPI(BaseAPI):
|
|
18
|
+
def __init__(self, apikey : str, **kwargs) -> None:
|
|
19
|
+
super().__init__(apikey)
|
|
20
|
+
|
|
21
|
+
# ! override default endpoint with endpoint
|
|
22
|
+
# ? the endpoint can be date (%Y-%m-%d) for historic rates
|
|
23
|
+
self._endpoint = kwargs.get("endpoint", "latest")
|
|
24
|
+
|
|
25
|
+
# ? each base class accepts parameters as keyword arguments
|
|
26
|
+
self._base = kwargs.get("base", "EUR")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def uri(self) -> str:
|
|
31
|
+
return "https://api.exchangeratesapi.io/v1/"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def endpoint(self) -> str:
|
|
36
|
+
# ? endpoint can be date (%Y-%m-%d) for historic rates
|
|
37
|
+
retval = self._endpoint
|
|
38
|
+
|
|
39
|
+
# if date object, then parse as string
|
|
40
|
+
if isinstance(retval, dt.date):
|
|
41
|
+
retval = retval.strftime("%Y-%m-%d")
|
|
42
|
+
|
|
43
|
+
return retval
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def params(self) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"base" : self._base,
|
|
50
|
+
"access_key" : self.apikey
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def headers(self) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get(self, **kwargs) -> Iterable:
|
|
60
|
+
return super().get(**kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def __format_error__(self, e : requests.exceptions.RequestException) -> str:
|
|
64
|
+
server_message = e.response.json()["error"]["message"]
|
|
65
|
+
return f"{e.response} : {e.response.reason} - {server_message}"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Base Class Wrapper and Structure for IO Integration
|
|
5
|
+
|
|
6
|
+
The base class is designed to be uniform for all integrated external
|
|
7
|
+
data sources with abstract class methods and properties.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from typing import Iterable
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
|
|
15
|
+
class BaseIO(ABC):
|
|
16
|
+
def __init__(self, data : dict | Iterable[dict]) -> None:
|
|
17
|
+
self.data = self.__parse_data__(data)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def __parse_data__(data : dict | Iterable[dict]) -> pd.DataFrame:
|
|
22
|
+
assert isinstance(data, (dict, list)), \
|
|
23
|
+
"Data must be a dictionary or a list of dictionaries."
|
|
24
|
+
|
|
25
|
+
return [data] if isinstance(data, dict) else data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def dataframe(self, *args, **kwargs) -> pd.DataFrame:
|
|
30
|
+
pass
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Operations to Parse Data as :mod:``pandas.DataFrame`` Object
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import datetime as dt
|
|
8
|
+
|
|
9
|
+
from tqdm import tqdm as TQ
|
|
10
|
+
from typing import Iterable
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from forexrates.io.base import BaseIO
|
|
15
|
+
|
|
16
|
+
class ExchangeRatesIO(BaseIO):
|
|
17
|
+
"""
|
|
18
|
+
An IO Parser for the API Response from ExchangeRatesAPI
|
|
19
|
+
|
|
20
|
+
The IO parser transforms the API response into a desired format
|
|
21
|
+
by calling any internal methods or processes.
|
|
22
|
+
|
|
23
|
+
A single response is received as a JSON (dictionary equivalent)
|
|
24
|
+
from the API and is parsed in a desired format.A typical response
|
|
25
|
+
structure is like:
|
|
26
|
+
|
|
27
|
+
.. code-block:: json
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
"base" : "EUR",
|
|
31
|
+
"date" : "2020-01-01",
|
|
32
|
+
"rates" : {
|
|
33
|
+
"USD" : 1.2345,
|
|
34
|
+
"INR" : 1.2345,
|
|
35
|
+
[...]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
In case of multiple response parsed under a single call, the
|
|
40
|
+
function is adjusted with parameter to handle such a response. A
|
|
41
|
+
sample response is like:
|
|
42
|
+
|
|
43
|
+
.. code-block:: json
|
|
44
|
+
|
|
45
|
+
[
|
|
46
|
+
{
|
|
47
|
+
"base" : "EUR",
|
|
48
|
+
"date" : "2020-01-01",
|
|
49
|
+
"rates" : {
|
|
50
|
+
"USD" : 1.2345,
|
|
51
|
+
"INR" : 1.2345,
|
|
52
|
+
[...]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
[...]
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
:type data: dict
|
|
59
|
+
:param data: A single JSON response from the ExchangeRatesAPI.
|
|
60
|
+
More information about the reponse is available here:
|
|
61
|
+
https://exchangeratesapi.io/documentation/.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, data : dict | Iterable[dict]) -> None:
|
|
65
|
+
super().__init__(data)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def dataframe(self, verbose : bool = False, **kwargs) -> pd.DataFrame:
|
|
69
|
+
"""
|
|
70
|
+
Parse a JSON (DICTIONARY) Response from ExchangeRatesAPI
|
|
71
|
+
|
|
72
|
+
The function provide method to parse a single/multiple
|
|
73
|
+
responses of the module and return a single
|
|
74
|
+
:mod:``pandas.DataFrame`` object.
|
|
75
|
+
|
|
76
|
+
:type verbose: bool
|
|
77
|
+
:param verbose: If set to ``True``, the function will print
|
|
78
|
+
additional information about the parsed dataframe.
|
|
79
|
+
|
|
80
|
+
Keyword Arguments
|
|
81
|
+
-----------------
|
|
82
|
+
Currently only setting the column and index names are supported
|
|
83
|
+
using keyword arguments. More control is available in the future
|
|
84
|
+
release and changes over dataframe.
|
|
85
|
+
|
|
86
|
+
* **index** (*str*): Name of the index column for parsed
|
|
87
|
+
dataframe. Defaults to ``foreign_exchange_rate``.
|
|
88
|
+
* **column** (*str*): Name of the column for parsed dataframe.
|
|
89
|
+
Defaults to ``target_currency_code``.
|
|
90
|
+
* **basecolumn** (*str*): Name of the base currency column for
|
|
91
|
+
parsed dataframe. Defaults to ``base_currency_code``.
|
|
92
|
+
* **datecolumn** (*str*): Name of the date column for parsed
|
|
93
|
+
dataframe. Defaults to ``effective_date``.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
:rtype: :mod:``pandas.DataFrame``
|
|
97
|
+
:return: A :mod:``pandas.DataFrame`` object with parsed data.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
index = kwargs.get("index", "foreign_exchange_rate")
|
|
101
|
+
column = kwargs.get("column", "target_currency_code")
|
|
102
|
+
basecolumn = kwargs.get("basecolumn", "base_currency_code")
|
|
103
|
+
datecolumn = kwargs.get("datecolumn", "effective_date")
|
|
104
|
+
|
|
105
|
+
frames = []
|
|
106
|
+
for data in TQ(self.data, desc = "Parsing Records..."):
|
|
107
|
+
base = data["base"]
|
|
108
|
+
date = data["date"]
|
|
109
|
+
|
|
110
|
+
# the data["date"] is a string in the format "%Y-%m-%d"
|
|
111
|
+
# convert it to a datetime object and return parsed frame
|
|
112
|
+
date = dt.datetime.strptime(date, "%Y-%m-%d")
|
|
113
|
+
|
|
114
|
+
frame = pd.DataFrame(
|
|
115
|
+
data["rates"], index = [index]
|
|
116
|
+
).T.reset_index().rename(columns = {"index" : column})
|
|
117
|
+
|
|
118
|
+
frame[basecolumn] = base
|
|
119
|
+
frame[datecolumn] = date
|
|
120
|
+
|
|
121
|
+
frames.append(frame[[datecolumn, basecolumn, column, index]])
|
|
122
|
+
|
|
123
|
+
frames = pd.concat(frames, ignore_index = True)
|
|
124
|
+
|
|
125
|
+
if verbose:
|
|
126
|
+
print(f"Total Parsed Records: {frames.shape[0]:,}")
|
|
127
|
+
|
|
128
|
+
min_ = frames[datecolumn].min().date()
|
|
129
|
+
max_ = frames[datecolumn].max().date()
|
|
130
|
+
print(f" >> Date Range: {min_} - {max_}")
|
|
131
|
+
|
|
132
|
+
unique_bases_= frames[basecolumn].unique()
|
|
133
|
+
n_unique_targets_ = frames[column].nunique()
|
|
134
|
+
print(f" >> Base Currency: {unique_bases_}")
|
|
135
|
+
print(f" >> # UQ Targets: {n_unique_targets_:,}")
|
|
136
|
+
|
|
137
|
+
return frames
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forexrates
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: A unified codebase for fetching FOREX rates.
|
|
5
|
+
Home-page: https://github.com/sharkutilities/forexrates
|
|
6
|
+
Author: shark-utilities developers
|
|
7
|
+
Author-email: shark-utilities developers <neuralNOD@outlook.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
Project-URL: Homepage, https://github.com/sharkutilities/forexrates
|
|
10
|
+
Project-URL: Issue Tracker, https://github.com/sharkutilities/forexrates/issues
|
|
11
|
+
Project-URL: Org. Homepage, https://github.com/sharkutilities
|
|
12
|
+
Keywords: forex,api,exchange rates,foreign exchange rates,wrappers,data science,data analysis,data scientist,data analyst
|
|
13
|
+
Classifier: Development Status :: 1 - Planning
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: Operating System :: Unix
|
|
20
|
+
Classifier: Operating System :: POSIX
|
|
21
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
22
|
+
Classifier: Programming Language :: Python
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: numpy==2.4.2
|
|
31
|
+
Requires-Dist: pandas==3.0.0
|
|
32
|
+
Requires-Dist: psycopg==3.3.2
|
|
33
|
+
Requires-Dist: psycopg-binary==3.3.2
|
|
34
|
+
Requires-Dist: requests==2.32.5
|
|
35
|
+
Requires-Dist: SQLAlchemy==2.0.0
|
|
36
|
+
Requires-Dist: tqdm==4.67.3
|
|
37
|
+
Dynamic: author
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
forexrates/__init__.py
|
|
5
|
+
forexrates.egg-info/PKG-INFO
|
|
6
|
+
forexrates.egg-info/SOURCES.txt
|
|
7
|
+
forexrates.egg-info/dependency_links.txt
|
|
8
|
+
forexrates.egg-info/requires.txt
|
|
9
|
+
forexrates.egg-info/top_level.txt
|
|
10
|
+
forexrates/api/__init__.py
|
|
11
|
+
forexrates/api/base.py
|
|
12
|
+
forexrates/api/exchangerates.py
|
|
13
|
+
forexrates/io/__init__.py
|
|
14
|
+
forexrates/io/base.py
|
|
15
|
+
forexrates/io/dataframe.py
|
|
16
|
+
main/config.py
|
|
17
|
+
main/erapi.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
A Set of Configuration Function(s) for Main Files
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import logging.handlers
|
|
9
|
+
|
|
10
|
+
def setLogger(
|
|
11
|
+
name : str = None,
|
|
12
|
+
level : int = logging.DEBUG,
|
|
13
|
+
filename : str = "./status.log"
|
|
14
|
+
) -> object:
|
|
15
|
+
"""
|
|
16
|
+
Setup a logger with pre-built configurations that can be used and
|
|
17
|
+
extended by any endpoints to capture data update, deletion and
|
|
18
|
+
other maintenance logs, using the :mod:`logger` module.
|
|
19
|
+
|
|
20
|
+
:type name: str
|
|
21
|
+
:param name: Name of the logger, else defaults to file name as the
|
|
22
|
+
name (``__name__`` of the execution script).
|
|
23
|
+
|
|
24
|
+
:type level: int
|
|
25
|
+
:param level: Any one of the allowed levels from the :mod:`logger`
|
|
26
|
+
can be used, defaults to debugging mode.
|
|
27
|
+
|
|
28
|
+
:type filename: str
|
|
29
|
+
:param filename: Full absolute/relative path of the logger file to
|
|
30
|
+
store the message, defaults to ``./status.log`` file.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(name or __name__)
|
|
34
|
+
logger.setLevel(level)
|
|
35
|
+
|
|
36
|
+
filehandler = logging.handlers.RotatingFileHandler(
|
|
37
|
+
filename,
|
|
38
|
+
maxBytes = 1024 * 1024,
|
|
39
|
+
backupCount = 1,
|
|
40
|
+
encoding = "utf8"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
formatter = logging.Formatter(
|
|
44
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
filehandler.setFormatter(formatter)
|
|
48
|
+
logger.addHandler(filehandler)
|
|
49
|
+
return logger
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
A Script to Update Data for ExchangeRatesIO on MacroDB Schema
|
|
5
|
+
|
|
6
|
+
Given a database schema with keys and other definitions intact as in
|
|
7
|
+
https://github.com/aivenio/macrodb, the query can be used to update
|
|
8
|
+
the data using CRON schedulers, or GitHub actions.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import datetime as dt
|
|
16
|
+
import sqlalchemy as sa
|
|
17
|
+
|
|
18
|
+
# append additional files, check actions for more information
|
|
19
|
+
sys.path.append(
|
|
20
|
+
os.path.join(os.path.dirname(__file__), "..")
|
|
21
|
+
)
|
|
22
|
+
sys.path.append("dtutils")
|
|
23
|
+
|
|
24
|
+
import forexrates # get the module from repository root
|
|
25
|
+
|
|
26
|
+
# https://ds-gringotts.readthedocs.io/en/latest/modules/utils/dtutils.html
|
|
27
|
+
import datetime_ as dt_ # cloned using git, ./dtutils
|
|
28
|
+
|
|
29
|
+
from config import setLogger # already exists in the current path
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
API_KEY = os.environ["EXCHANGERATES_IO_API_KEY"]
|
|
33
|
+
|
|
34
|
+
# create a logger for the erapi module
|
|
35
|
+
logger = setLogger(name = "ERAPI")
|
|
36
|
+
|
|
37
|
+
# get configurations for database connection elements, and build
|
|
38
|
+
DATABASE = os.environ["AIVENIO_MACRODB_DATABASE"]
|
|
39
|
+
HOSTNAME = os.environ["AIVENIO_MACRODB_HOSTNAME"]
|
|
40
|
+
PASSWORD = os.environ["AIVENIO_MACRODB_PASSWORD"]
|
|
41
|
+
PORTNAME = os.environ["AIVENIO_MACRODB_PORTNAME"]
|
|
42
|
+
USERNAME = os.environ["AIVENIO_MACRODB_USERNAME"]
|
|
43
|
+
|
|
44
|
+
engine = sa.create_engine(
|
|
45
|
+
f"postgresql+psycopg://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORTNAME}/{DATABASE}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
engine.connect()
|
|
50
|
+
except Exception as err:
|
|
51
|
+
logger.critical(f"Cannot connect to database. Error: {err}")
|
|
52
|
+
else:
|
|
53
|
+
logger.info(f"Connection established to {DATABASE}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# get the last available date from the database, and then set a
|
|
57
|
+
# context to fetch the date period for which data is required
|
|
58
|
+
statement = """
|
|
59
|
+
SELECT MAX(effective_date)::DATE AS last_date
|
|
60
|
+
FROM common.forex_rate_tx
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
with engine.connect() as connection:
|
|
64
|
+
start_date = connection.execute(
|
|
65
|
+
sa.text(statement)
|
|
66
|
+
).fetchone()[0]
|
|
67
|
+
start_date += dt.timedelta(days = 1)
|
|
68
|
+
|
|
69
|
+
last_date = dt.datetime.now().date() - dt.timedelta(days = 1)
|
|
70
|
+
dates = list(dt_.date_range(start = start_date, end = last_date))
|
|
71
|
+
|
|
72
|
+
logger.info(f"Trying to fetch data from {start_date} to {last_date}")
|
|
73
|
+
logger.info(f"This will consume {len(dates):,} calls for the API")
|
|
74
|
+
|
|
75
|
+
data = [
|
|
76
|
+
forexrates.api.ExchangeRatesAPI(
|
|
77
|
+
apikey = API_KEY, endpoint = date.strftime("%Y-%m-%d")
|
|
78
|
+
).get(
|
|
79
|
+
verify = False, suppresswarning = True
|
|
80
|
+
) for date in dates
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
parser = forexrates.io.dataframe.ExchangeRatesIO(data)
|
|
84
|
+
dataframe = parser.dataframe(
|
|
85
|
+
index = "exchange_rate", verbose = True
|
|
86
|
+
)
|
|
87
|
+
dataframe["data_source_id"] = "ERAPI"
|
|
88
|
+
|
|
89
|
+
with engine.connect() as connection:
|
|
90
|
+
metadata = sa.Table(
|
|
91
|
+
"forex_rate_tx", sa.MetaData(schema = "common"),
|
|
92
|
+
autoload_with = connection
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
connection.execute(metadata.insert(), dataframe.to_dict(orient = "records"))
|
|
96
|
+
connection.commit()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"numpy==2.4.2",
|
|
4
|
+
"pandas==3.0.0",
|
|
5
|
+
"psycopg==3.3.2",
|
|
6
|
+
"psycopg-binary==3.3.2",
|
|
7
|
+
"requests==2.32.5",
|
|
8
|
+
"setuptools==82.0.0",
|
|
9
|
+
"SQLAlchemy==2.0.0",
|
|
10
|
+
"tqdm==4.67.3",
|
|
11
|
+
"wheel==0.46.3",
|
|
12
|
+
]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
14
|
+
|
|
15
|
+
[project]
|
|
16
|
+
name = "forexrates"
|
|
17
|
+
dynamic = ["version"]
|
|
18
|
+
authors = [
|
|
19
|
+
{ name = "shark-utilities developers", email = "neuralNOD@outlook.com" }
|
|
20
|
+
]
|
|
21
|
+
description = "A unified codebase for fetching FOREX rates."
|
|
22
|
+
# readme = "README.md" # Uncomment if you have a README.md
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
|
+
license = { text = "MIT License" }
|
|
25
|
+
keywords = [
|
|
26
|
+
"forex", "api", "exchange rates", "foreign exchange rates",
|
|
27
|
+
"wrappers", "data science", "data analysis", "data scientist", "data analyst"
|
|
28
|
+
]
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 1 - Planning",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"Intended Audience :: Education",
|
|
33
|
+
"Intended Audience :: End Users/Desktop",
|
|
34
|
+
"Intended Audience :: Information Technology",
|
|
35
|
+
"Intended Audience :: Science/Research",
|
|
36
|
+
"Operating System :: Unix",
|
|
37
|
+
"Operating System :: POSIX",
|
|
38
|
+
"Operating System :: Microsoft :: Windows",
|
|
39
|
+
"Programming Language :: Python",
|
|
40
|
+
"Programming Language :: Python :: 3",
|
|
41
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
42
|
+
"Programming Language :: Python :: 3.11",
|
|
43
|
+
"Programming Language :: Python :: 3.12",
|
|
44
|
+
"License :: OSI Approved :: MIT License",
|
|
45
|
+
]
|
|
46
|
+
dependencies = [
|
|
47
|
+
"numpy==2.4.2",
|
|
48
|
+
"pandas==3.0.0",
|
|
49
|
+
"psycopg==3.3.2",
|
|
50
|
+
"psycopg-binary==3.3.2",
|
|
51
|
+
"requests==2.32.5",
|
|
52
|
+
"SQLAlchemy==2.0.0",
|
|
53
|
+
"tqdm==4.67.3",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
"Homepage" = "https://github.com/sharkutilities/forexrates"
|
|
58
|
+
"Issue Tracker" = "https://github.com/sharkutilities/forexrates/issues"
|
|
59
|
+
"Org. Homepage" = "https://github.com/sharkutilities"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools]
|
|
62
|
+
py-modules = []
|
|
63
|
+
|
|
64
|
+
[tool.setuptools.packages.find]
|
|
65
|
+
exclude = ["tests*", "examples*"]
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.dynamic]
|
|
68
|
+
version = {attr = "forexrates.__version__"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#
|
|
3
|
+
# Copywright (C) 2024 Debmalya Pramanik <neuralNOD@gmail.com>
|
|
4
|
+
# LICENSE: MIT License
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from setuptools import setup
|
|
10
|
+
from setuptools import find_packages
|
|
11
|
+
|
|
12
|
+
sys.path.append(
|
|
13
|
+
os.path.join(os.path.dirname(__file__))
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
import forexrates
|
|
17
|
+
|
|
18
|
+
setup(
|
|
19
|
+
name = "forexrates",
|
|
20
|
+
version = forexrates.__version__,
|
|
21
|
+
author = "shark-utilities developers",
|
|
22
|
+
author_email = "neuralNOD@outlook.com",
|
|
23
|
+
description = "A unified codebase for fetching FOREX rates.",
|
|
24
|
+
# long_description = open("README.md", "r").read(),
|
|
25
|
+
# long_description_content_type = "text/markdown",
|
|
26
|
+
url = "https://github.com/sharkutilities/forexrates",
|
|
27
|
+
packages = find_packages(
|
|
28
|
+
exclude = ["tests*", "examples*"]
|
|
29
|
+
),
|
|
30
|
+
install_requires = [
|
|
31
|
+
"numpy==2.4.2",
|
|
32
|
+
"pandas==3.0.0",
|
|
33
|
+
"psycopg==3.3.2",
|
|
34
|
+
"psycopg-binary==3.3.2",
|
|
35
|
+
"requests==2.32.5",
|
|
36
|
+
"SQLAlchemy==2.0.0",
|
|
37
|
+
"tqdm==4.67.3",
|
|
38
|
+
],
|
|
39
|
+
classifiers = [
|
|
40
|
+
"Development Status :: 1 - Planning",
|
|
41
|
+
"Intended Audience :: Developers",
|
|
42
|
+
"Intended Audience :: Education",
|
|
43
|
+
"Intended Audience :: End Users/Desktop",
|
|
44
|
+
"Intended Audience :: Information Technology",
|
|
45
|
+
"Intended Audience :: Science/Research",
|
|
46
|
+
"Operating System :: Unix",
|
|
47
|
+
"Operating System :: POSIX",
|
|
48
|
+
"Operating System :: Microsoft :: Windows",
|
|
49
|
+
"Programming Language :: Python",
|
|
50
|
+
"Programming Language :: Python :: 3",
|
|
51
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
52
|
+
"Programming Language :: Python :: 3.10",
|
|
53
|
+
"Programming Language :: Python :: 3.11",
|
|
54
|
+
"Programming Language :: Python :: 3.12",
|
|
55
|
+
"License :: OSI Approved :: MIT License"
|
|
56
|
+
],
|
|
57
|
+
project_urls = {
|
|
58
|
+
"Issue Tracker" : "https://github.com/sharkutilities/forexrates/issues",
|
|
59
|
+
# "Code Documentations" : "https://.readthedocs.io/en/latest/index.html",
|
|
60
|
+
"Org. Homepage" : "https://github.com/sharkutilities"
|
|
61
|
+
},
|
|
62
|
+
keywords = [
|
|
63
|
+
# keywords for finding the package::
|
|
64
|
+
"forex", "api", "exchange rates", "foreign exchange rates",
|
|
65
|
+
# keywords for finding the package relevant to usecases::
|
|
66
|
+
"wrappers", "data science", "data analysis", "data scientist", "data analyst"
|
|
67
|
+
],
|
|
68
|
+
python_requires = ">=3.11"
|
|
69
|
+
)
|