rate-api-python 1.0.0__py3-none-any.whl → 1.0.2__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 +16 -6
- rate_api_python-1.0.2.dist-info/METADATA +124 -0
- rate_api_python-1.0.2.dist-info/RECORD +6 -0
- rate_api_python-1.0.0.dist-info/METADATA +0 -71
- rate_api_python-1.0.0.dist-info/RECORD +0 -6
- {rate_api_python-1.0.0.dist-info → rate_api_python-1.0.2.dist-info}/WHEEL +0 -0
- {rate_api_python-1.0.0.dist-info → rate_api_python-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {rate_api_python-1.0.0.dist-info → rate_api_python-1.0.2.dist-info}/top_level.txt +0 -0
rate_api/__init__.py
CHANGED
|
@@ -12,7 +12,7 @@ import urllib.error
|
|
|
12
12
|
import urllib.parse
|
|
13
13
|
import urllib.request
|
|
14
14
|
|
|
15
|
-
__version__ = "1.0.
|
|
15
|
+
__version__ = "1.0.2"
|
|
16
16
|
__all__ = ["RateApiClient", "RateApiError", "RateLimitError", "RateApiTimeoutError"]
|
|
17
17
|
|
|
18
18
|
|
|
@@ -116,10 +116,16 @@ class RateApiClient:
|
|
|
116
116
|
def _get(self, endpoint, query=None):
|
|
117
117
|
return self._request(f"{self.base_url}/{self.api_key}/{endpoint}", query or {})
|
|
118
118
|
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _normalize_headers(headers):
|
|
121
|
+
# urllib's HTTPMessage.get() is case-insensitive, but dict(...) is not — and
|
|
122
|
+
# Cloudflare/HTTP-2 send header names lowercased. Normalize to lowercase keys.
|
|
123
|
+
return {k.lower(): v for k, v in dict(headers or {}).items()}
|
|
124
|
+
|
|
119
125
|
@staticmethod
|
|
120
126
|
def _retry_after(headers, default=60):
|
|
121
127
|
try:
|
|
122
|
-
return int(headers.get("
|
|
128
|
+
return int(headers.get("retry-after", default))
|
|
123
129
|
except (TypeError, ValueError):
|
|
124
130
|
return default
|
|
125
131
|
|
|
@@ -148,11 +154,15 @@ class RateApiClient:
|
|
|
148
154
|
try:
|
|
149
155
|
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
150
156
|
status = resp.status
|
|
151
|
-
resp_headers =
|
|
152
|
-
|
|
157
|
+
resp_headers = self._normalize_headers(resp.headers)
|
|
158
|
+
raw = resp.read().decode("utf-8")
|
|
159
|
+
try:
|
|
160
|
+
data = json.loads(raw)
|
|
161
|
+
except ValueError:
|
|
162
|
+
raise RateApiError(f"Invalid JSON response (HTTP {status}).", status)
|
|
153
163
|
except urllib.error.HTTPError as e:
|
|
154
164
|
status = e.code
|
|
155
|
-
resp_headers =
|
|
165
|
+
resp_headers = self._normalize_headers(e.headers)
|
|
156
166
|
# Retry transient statuses, honoring Retry-After.
|
|
157
167
|
if status in (429, 503) and attempt < self.max_retries:
|
|
158
168
|
attempt += 1
|
|
@@ -176,7 +186,7 @@ class RateApiClient:
|
|
|
176
186
|
err = data.get("error") or {}
|
|
177
187
|
msg = err.get("message") or data.get("message") or "Unknown API error"
|
|
178
188
|
etype = err.get("type")
|
|
179
|
-
rid = data.get("request_id") or resp_headers.get("
|
|
189
|
+
rid = data.get("request_id") or resp_headers.get("x-request-id")
|
|
180
190
|
if status == 429:
|
|
181
191
|
raise RateLimitError(msg, status, etype, rid, self._retry_after(resp_headers))
|
|
182
192
|
raise RateApiError(msg, status, etype, rid)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rate-api-python
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Official Python client for the Rate-API.com exchange-rate & crypto API
|
|
5
|
+
Author-email: Vilgar <digitalbrainsam@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://rate-api.com
|
|
8
|
+
Project-URL: Documentation, https://rate-api.com/en/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/Vilgar/rate-api.com
|
|
10
|
+
Project-URL: Issues, https://rate-api.com/en/docs
|
|
11
|
+
Keywords: exchange-rate,currency,forex,crypto,api,rate-api
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# rate-api-python
|
|
30
|
+
|
|
31
|
+
Official Python client for [Rate-API.com](https://rate-api.com). Standard library only — no dependencies. Python 3.8+.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install rate-api-python
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from rate_api import RateApiClient, RateApiError
|
|
43
|
+
|
|
44
|
+
client = RateApiClient("YOUR_API_KEY")
|
|
45
|
+
|
|
46
|
+
rates = client.latest("USD", ["EUR", "GBP"])
|
|
47
|
+
print(rates["rates"]["EUR"])
|
|
48
|
+
|
|
49
|
+
client.convert("USD", "EUR", 100) # Pro+
|
|
50
|
+
client.historical("2026-01-15", "USD", ["EUR"]) # Pro+
|
|
51
|
+
client.pair("USD", "EUR") # single pair
|
|
52
|
+
client.timeseries("2026-01-01", "2026-01-31") # Business+
|
|
53
|
+
client.fluctuation("2026-01-01", "2026-01-31") # Business+
|
|
54
|
+
client.crypto(["BTC", "ETH"]) # Pro+
|
|
55
|
+
client.currencies() # supported currencies
|
|
56
|
+
client.health() # public
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
client.timeseries("2020-01-01", "2026-12-31")
|
|
60
|
+
except RateApiError as e:
|
|
61
|
+
print(e, e.status) # "Date range too large. Maximum is 366 days." 400
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## v2 features
|
|
65
|
+
|
|
66
|
+
The client exposes the v2 endpoints directly (they resolve to `/api/v2` regardless of base URL):
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Latest with 24h change, metadata and precision
|
|
70
|
+
r = client.latest_v2("USD", ["EUR", "GBP"], include_change=True, include_metadata=True, precision=4)
|
|
71
|
+
print(r["changes_pct"]["EUR"])
|
|
72
|
+
|
|
73
|
+
# Historical comparison between two dates (Pro+)
|
|
74
|
+
cmp = client.historical_compare("2026-01-15", "2026-01-01", "USD", ["EUR"])
|
|
75
|
+
|
|
76
|
+
# Batch conversion — up to 100 pairs in one call (Pro+)
|
|
77
|
+
batch = client.batch_convert([
|
|
78
|
+
{"from": "USD", "to": "EUR", "amount": 100},
|
|
79
|
+
{"from": "GBP", "to": "JPY", "amount": 50},
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
# Your configured rate alerts (Business+)
|
|
83
|
+
alerts = client.alerts()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Errors
|
|
87
|
+
|
|
88
|
+
Every failure raises `RateApiError` (or a subclass), so a single `except RateApiError` catches everything:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import time
|
|
92
|
+
from rate_api import RateApiClient, RateApiError, RateLimitError, RateApiTimeoutError
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
client.latest("USD", ["EUR"])
|
|
96
|
+
except RateLimitError as e:
|
|
97
|
+
time.sleep(e.retry_after) # honour the server's backoff
|
|
98
|
+
except RateApiTimeoutError:
|
|
99
|
+
... # request exceeded `timeout`
|
|
100
|
+
except RateApiError as e:
|
|
101
|
+
print(e, e.status, e.type, e.request_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Class | When | Extra attributes |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `RateApiError` | any API/HTTP error | `status` (HTTP code; `None` on network/timeout), `type` (stable `error.type` slug), `request_id` (`X-Request-Id` — quote it when contacting support) |
|
|
107
|
+
| `RateLimitError` | HTTP 429 | `retry_after` (seconds to wait) |
|
|
108
|
+
| `RateApiTimeoutError` | request exceeded `timeout` | — |
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
RateApiClient(api_key, base_url="https://rate-api.com/api/v1", timeout=15, max_retries=2)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Argument | Default | Notes |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `base_url` | `https://rate-api.com/api/v1` | pass `…/api/v2` to target v2 directly |
|
|
119
|
+
| `timeout` | `15` | per-request socket timeout in seconds (covers headers and body) |
|
|
120
|
+
| `max_retries` | `2` | automatically retries 429/503/network/timeout with exponential backoff, honouring the server's `Retry-After` header |
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
rate_api/__init__.py,sha256=xSMMweBtuXdST6akKoehVNA69X6omsVmwEtDzclie-U,8314
|
|
2
|
+
rate_api_python-1.0.2.dist-info/licenses/LICENSE,sha256=nCjIn_oDw98qUrGtfT5o19Qk02nDpkr3xbKgFtu3Z0A,1069
|
|
3
|
+
rate_api_python-1.0.2.dist-info/METADATA,sha256=2FJ6y1wYPZdAGji2Cqs4K0xA3s2C5369mf71MI7VAo8,4357
|
|
4
|
+
rate_api_python-1.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
rate_api_python-1.0.2.dist-info/top_level.txt,sha256=y8GBQa9yzwxT17UjU9xreA4yPbHqup7c0ryMbx1gYzI,9
|
|
6
|
+
rate_api_python-1.0.2.dist-info/RECORD,,
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|
|
@@ -1,6 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|