rate-api-python 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rate_api/__init__.py ADDED
@@ -0,0 +1,183 @@
1
+ """Official Python client for the Rate-API.com exchange-rate & crypto API.
2
+
3
+ Uses only the standard library (urllib) — no dependencies.
4
+
5
+ from rate_api import RateApiClient
6
+ rates = RateApiClient("YOUR_API_KEY").latest("USD", ["EUR", "GBP"])
7
+ """
8
+
9
+ import json
10
+ import time
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+
15
+ __version__ = "1.0.0"
16
+ __all__ = ["RateApiClient", "RateApiError", "RateLimitError", "RateApiTimeoutError"]
17
+
18
+
19
+ class RateApiError(Exception):
20
+ def __init__(self, message, status=None, type=None, request_id=None):
21
+ super().__init__(message)
22
+ self.status = status # HTTP status (None for network/timeout)
23
+ self.type = type # stable error.type slug from the API
24
+ self.request_id = request_id # X-Request-Id for support correlation
25
+
26
+
27
+ class RateLimitError(RateApiError):
28
+ """Raised on HTTP 429; carries retry_after (seconds)."""
29
+ def __init__(self, message, status=None, type=None, request_id=None, retry_after=60):
30
+ super().__init__(message, status, type, request_id)
31
+ self.retry_after = retry_after
32
+
33
+
34
+ class RateApiTimeoutError(RateApiError):
35
+ """Raised when a request exceeds the timeout."""
36
+
37
+
38
+ class RateApiClient:
39
+ def __init__(self, api_key, base_url="https://rate-api.com/api/v1", timeout=15, max_retries=2):
40
+ if not api_key:
41
+ raise RateApiError("An API key is required.")
42
+ self.api_key = api_key
43
+ self.base_url = base_url.rstrip("/")
44
+ self.timeout = timeout
45
+ self.max_retries = max_retries
46
+
47
+ def latest(self, base="USD", symbols=None):
48
+ return self._get("latest", self._with_symbols({"base": base}, symbols))
49
+
50
+ def convert(self, from_currency, to_currency, amount):
51
+ return self._get("convert", {"from": from_currency, "to": to_currency, "amount": amount})
52
+
53
+ def historical(self, date, base="USD", symbols=None):
54
+ return self._get("historical", self._with_symbols({"date": date, "base": base}, symbols))
55
+
56
+ def pair(self, from_currency, to_currency):
57
+ return self._get(f"pair/{urllib.parse.quote(from_currency)}/{urllib.parse.quote(to_currency)}")
58
+
59
+ def timeseries(self, start_date, end_date, base="USD", symbols=None):
60
+ return self._get("timeseries", self._with_symbols(
61
+ {"start_date": start_date, "end_date": end_date, "base": base}, symbols))
62
+
63
+ def fluctuation(self, start_date, end_date, base="USD", symbols=None):
64
+ return self._get("fluctuation", self._with_symbols(
65
+ {"start_date": start_date, "end_date": end_date, "base": base}, symbols))
66
+
67
+ def crypto(self, symbols=None):
68
+ query = {"symbols": ",".join(symbols)} if symbols else {}
69
+ return self._get("crypto", query)
70
+
71
+ def currencies(self):
72
+ return self._get("currencies")
73
+
74
+ def health(self):
75
+ return self._request(f"{self.base_url}/health", {})
76
+
77
+ # ---- v2 endpoints (resolve to /api/v2 regardless of the configured base) ----
78
+
79
+ def _v2_base(self):
80
+ if self.base_url.endswith("/v1"):
81
+ return self.base_url[:-3] + "/v2"
82
+ return self.base_url.replace("/v1/", "/v2/")
83
+
84
+ def latest_v2(self, base="USD", symbols=None, include_metadata=False, include_change=False, precision=None):
85
+ """v2 latest with metadata / 24h change / precision options."""
86
+ query = self._with_symbols({"base": base}, symbols)
87
+ if include_metadata:
88
+ query["include_metadata"] = "true"
89
+ if include_change:
90
+ query["include_change"] = "true"
91
+ if precision is not None:
92
+ query["precision"] = precision
93
+ return self._request(f"{self._v2_base()}/{self.api_key}/latest", query)
94
+
95
+ def historical_compare(self, date, compare_date=None, base="USD", symbols=None):
96
+ """v2 historical with an optional compare_date for per-currency deltas (Pro+)."""
97
+ query = self._with_symbols({"date": date, "base": base}, symbols)
98
+ if compare_date:
99
+ query["compare_date"] = compare_date
100
+ return self._request(f"{self._v2_base()}/{self.api_key}/historical", query)
101
+
102
+ def batch_convert(self, conversions):
103
+ """v2 batch conversion: list of {"from","to","amount"} dicts (Pro+). Max 100."""
104
+ return self._request(f"{self._v2_base()}/{self.api_key}/batch-convert", {}, method="POST", body=conversions)
105
+
106
+ def alerts(self):
107
+ """List your configured rate alerts (Business+). Manage them in the dashboard."""
108
+ return self._request(f"{self._v2_base()}/{self.api_key}/alerts", {})
109
+
110
+ @staticmethod
111
+ def _with_symbols(query, symbols):
112
+ if symbols:
113
+ query["symbols"] = ",".join(symbols)
114
+ return query
115
+
116
+ def _get(self, endpoint, query=None):
117
+ return self._request(f"{self.base_url}/{self.api_key}/{endpoint}", query or {})
118
+
119
+ @staticmethod
120
+ def _retry_after(headers, default=60):
121
+ try:
122
+ return int(headers.get("Retry-After", default))
123
+ except (TypeError, ValueError):
124
+ return default
125
+
126
+ def _backoff(self, attempt, headers):
127
+ ra = self._retry_after(headers, default=0)
128
+ return min(ra, 30) if ra else min(2 ** (attempt - 1), 8)
129
+
130
+ def _request(self, url, query, method="GET", body=None):
131
+ if query:
132
+ url += "?" + urllib.parse.urlencode(query)
133
+ headers = {
134
+ "Accept": "application/json",
135
+ "X-API-Key": self.api_key,
136
+ "User-Agent": "rate-api-python/1.0",
137
+ }
138
+ data_bytes = None
139
+ if body is not None:
140
+ data_bytes = json.dumps(body).encode("utf-8")
141
+ headers["Content-Type"] = "application/json"
142
+
143
+ attempt = 0
144
+ while True:
145
+ req = urllib.request.Request(url, data=data_bytes, method=method, headers=headers)
146
+ status = None
147
+ resp_headers = {}
148
+ try:
149
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
150
+ status = resp.status
151
+ resp_headers = dict(resp.headers)
152
+ data = json.loads(resp.read().decode("utf-8"))
153
+ except urllib.error.HTTPError as e:
154
+ status = e.code
155
+ resp_headers = dict(e.headers or {})
156
+ # Retry transient statuses, honoring Retry-After.
157
+ if status in (429, 503) and attempt < self.max_retries:
158
+ attempt += 1
159
+ time.sleep(self._backoff(attempt, resp_headers))
160
+ continue
161
+ try:
162
+ data = json.loads(e.read().decode("utf-8")) # API returns JSON error bodies
163
+ except Exception:
164
+ raise RateApiError(f"HTTP {status}", status)
165
+ except OSError as e: # URLError, socket.timeout, TimeoutError all subclass OSError
166
+ if attempt < self.max_retries:
167
+ attempt += 1
168
+ time.sleep(self._backoff(attempt, {}))
169
+ continue
170
+ reason = str(getattr(e, "reason", e))
171
+ if isinstance(e, TimeoutError) or "timed out" in reason.lower():
172
+ raise RateApiTimeoutError(f"Request timed out after {self.timeout}s")
173
+ raise RateApiError(f"Request failed: {reason}")
174
+
175
+ if isinstance(data, dict) and data.get("success") is False:
176
+ err = data.get("error") or {}
177
+ msg = err.get("message") or data.get("message") or "Unknown API error"
178
+ etype = err.get("type")
179
+ rid = data.get("request_id") or resp_headers.get("X-Request-Id")
180
+ if status == 429:
181
+ raise RateLimitError(msg, status, etype, rid, self._retry_after(resp_headers))
182
+ raise RateApiError(msg, status, etype, rid)
183
+ return data
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: rate-api-python
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the Rate-API.com exchange-rate & crypto API
5
+ License: MIT
6
+ Project-URL: Homepage, https://rate-api.com
7
+ Project-URL: Documentation, https://rate-api.com/en/docs
8
+ Project-URL: Repository, https://github.com/Vilgar/rate-api.com
9
+ Keywords: exchange-rate,currency,forex,crypto,api,rate-api
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+ # rate-api-python
16
+
17
+ Official Python client for [Rate-API.com](https://rate-api.com). Standard library only — no dependencies. Python 3.8+.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install rate-api-python
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ from rate_api import RateApiClient, RateApiError
29
+
30
+ client = RateApiClient("YOUR_API_KEY")
31
+
32
+ rates = client.latest("USD", ["EUR", "GBP"])
33
+ print(rates["rates"]["EUR"])
34
+
35
+ client.convert("USD", "EUR", 100) # Pro+
36
+ client.historical("2026-01-15", "USD", ["EUR"]) # Pro+
37
+ client.timeseries("2026-01-01", "2026-01-31") # Business+
38
+ client.crypto(["BTC", "ETH"]) # Pro+
39
+ client.health() # public
40
+
41
+ try:
42
+ client.timeseries("2020-01-01", "2026-12-31")
43
+ except RateApiError as e:
44
+ print(e, e.status) # "Date range too large. Maximum is 366 days." 400
45
+ ```
46
+
47
+ ## v2 features
48
+
49
+ The client exposes the v2 endpoints directly (they resolve to `/api/v2` regardless of base URL):
50
+
51
+ ```python
52
+ # Latest with 24h change, metadata and precision
53
+ r = client.latest_v2("USD", ["EUR", "GBP"], include_change=True, include_metadata=True, precision=4)
54
+ print(r["changes_pct"]["EUR"])
55
+
56
+ # Historical comparison between two dates (Pro+)
57
+ cmp = client.historical_compare("2026-01-15", "2026-01-01", "USD", ["EUR"])
58
+
59
+ # Batch conversion — up to 100 pairs in one call (Pro+)
60
+ batch = client.batch_convert([
61
+ {"from": "USD", "to": "EUR", "amount": 100},
62
+ {"from": "GBP", "to": "JPY", "amount": 50},
63
+ ])
64
+
65
+ # Your configured rate alerts (Business+)
66
+ alerts = client.alerts()
67
+ ```
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,6 @@
1
+ rate_api/__init__.py,sha256=J9mVlZ0hKr_S1jq3mnXNBnZFTwa5ro_EswEcR1IipXo,7806
2
+ rate_api_python-1.0.0.dist-info/licenses/LICENSE,sha256=nCjIn_oDw98qUrGtfT5o19Qk02nDpkr3xbKgFtu3Z0A,1069
3
+ rate_api_python-1.0.0.dist-info/METADATA,sha256=0Z9I8Lyh_JiGDXwFhCX6fPobnXUnUaFug01nTh1VCkY,2030
4
+ rate_api_python-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ rate_api_python-1.0.0.dist-info/top_level.txt,sha256=y8GBQa9yzwxT17UjU9xreA4yPbHqup7c0ryMbx1gYzI,9
6
+ rate_api_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rate-API.com
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 @@
1
+ rate_api