eltoque-python 0.1.0__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.
- eltoque_python-0.1.0/.gitignore +28 -0
- eltoque_python-0.1.0/CHANGELOG.md +34 -0
- eltoque_python-0.1.0/LICENSE +24 -0
- eltoque_python-0.1.0/PKG-INFO +286 -0
- eltoque_python-0.1.0/README.md +258 -0
- eltoque_python-0.1.0/docs/api-contract.md +49 -0
- eltoque_python-0.1.0/docs/configuration.md +74 -0
- eltoque_python-0.1.0/docs/errors.md +61 -0
- eltoque_python-0.1.0/docs/publishing.md +69 -0
- eltoque_python-0.1.0/docs/quickstart.es.md +127 -0
- eltoque_python-0.1.0/docs/security.md +38 -0
- eltoque_python-0.1.0/examples/async_basic.py +20 -0
- eltoque_python-0.1.0/examples/basic.py +23 -0
- eltoque_python-0.1.0/examples/date_range.py +24 -0
- eltoque_python-0.1.0/pyproject.toml +89 -0
- eltoque_python-0.1.0/src/eltoque/__init__.py +44 -0
- eltoque_python-0.1.0/src/eltoque/_async_client.py +131 -0
- eltoque_python-0.1.0/src/eltoque/_client.py +136 -0
- eltoque_python-0.1.0/src/eltoque/_config.py +106 -0
- eltoque_python-0.1.0/src/eltoque/_dates.py +53 -0
- eltoque_python-0.1.0/src/eltoque/_retry.py +59 -0
- eltoque_python-0.1.0/src/eltoque/_transport.py +165 -0
- eltoque_python-0.1.0/src/eltoque/_version.py +1 -0
- eltoque_python-0.1.0/src/eltoque/exceptions.py +77 -0
- eltoque_python-0.1.0/src/eltoque/models.py +78 -0
- eltoque_python-0.1.0/src/eltoque/py.typed +0 -0
- eltoque_python-0.1.0/tests/__init__.py +0 -0
- eltoque_python-0.1.0/tests/conftest.py +39 -0
- eltoque_python-0.1.0/tests/test_async_client.py +195 -0
- eltoque_python-0.1.0/tests/test_client.py +269 -0
- eltoque_python-0.1.0/tests/test_concurrency.py +102 -0
- eltoque_python-0.1.0/tests/test_config.py +119 -0
- eltoque_python-0.1.0/tests/test_dates.py +73 -0
- eltoque_python-0.1.0/tests/test_docs.py +67 -0
- eltoque_python-0.1.0/tests/test_errors.py +57 -0
- eltoque_python-0.1.0/tests/test_failures.py +116 -0
- eltoque_python-0.1.0/tests/test_integration.py +36 -0
- eltoque_python-0.1.0/tests/test_models.py +29 -0
- eltoque_python-0.1.0/tests/test_parity.py +99 -0
- eltoque_python-0.1.0/tests/test_public_api.py +24 -0
- eltoque_python-0.1.0/tests/test_rate_limits.py +38 -0
- eltoque_python-0.1.0/tests/test_retry.py +80 -0
- eltoque_python-0.1.0/tests/test_security.py +129 -0
- eltoque_python-0.1.0/tests/test_transport.py +187 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
.coverage.*
|
|
17
|
+
htmlcov/
|
|
18
|
+
coverage.xml
|
|
19
|
+
|
|
20
|
+
# Secrets and local config
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
23
|
+
!.env.example
|
|
24
|
+
|
|
25
|
+
# Editors and OS
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/
|
|
28
|
+
.DS_Store
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/). Before 1.0, minor versions may contain breaking
|
|
6
|
+
changes; they will always be listed here. The SDK version is independent of the elTOQUE API
|
|
7
|
+
version (`1.0.beta`).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.1.0] - 2026-10-22
|
|
12
|
+
|
|
13
|
+
First release. Unpublished until the repository owner approves it (see docs/publishing.md).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `ElToqueClient` and `AsyncElToqueClient` for the documented `GET /v1/trmi` operation:
|
|
18
|
+
`get_latest_rates()`, `get_rates(date_from, date_to)` and `get_rates_for_day(day)`.
|
|
19
|
+
- `RatesResponse` envelope that exposes the provider JSON untouched, plus `RateLimitInfo`
|
|
20
|
+
parsed from `X-RateLimit-*` and `Retry-After` headers.
|
|
21
|
+
- Exception hierarchy rooted at `ElToqueError`, with one subclass per documented HTTP status.
|
|
22
|
+
- Strict validation: naive datetimes only, `date_from < date_to`, window shorter than 24 hours.
|
|
23
|
+
- Opt-in, bounded retries (`max_retries`, default 0) honoring `Retry-After`.
|
|
24
|
+
- Safety defaults: HTTPS-only base URL, redirects never followed, 5 MiB response cap, token
|
|
25
|
+
redacted from `repr`, errors and logs.
|
|
26
|
+
- Injectable `httpx` client/transport with explicit ownership semantics.
|
|
27
|
+
- Offline test suite (`httpx.MockTransport`), opt-in live integration test, README and Spanish
|
|
28
|
+
quickstart, configuration/errors/security guides, runnable examples.
|
|
29
|
+
|
|
30
|
+
### Known limitations
|
|
31
|
+
|
|
32
|
+
- The provider does not publish the HTTP 200 schema; `RatesResponse.data` is untyped.
|
|
33
|
+
- The provider's time zone is undocumented.
|
|
34
|
+
- Not yet verified against the real API (no authorized token was used).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 eltoque-python contributors
|
|
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.
|
|
22
|
+
|
|
23
|
+
Scope note: this license covers the source code of this repository only. It
|
|
24
|
+
grants no rights over the elTOQUE API, data, name or trademarks.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: eltoque-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial, typed Python client for the official elTOQUE exchange-rate API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Alejo2075/eltoque-python
|
|
6
|
+
Project-URL: Source, https://github.com/Alejo2075/eltoque-python
|
|
7
|
+
Project-URL: Issues, https://github.com/Alejo2075/eltoque-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Alejo2075/eltoque-python/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: elTOQUE API docs, https://tasas.eltoque.com/docs/
|
|
10
|
+
Author: eltoque-python contributors
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api-client,async,cuba,eltoque,exchange-rate,httpx
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx<1,>=0.28
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# eltoque-python
|
|
30
|
+
|
|
31
|
+
**Unofficial Python SDK for elTOQUE exchange-rate data.**
|
|
32
|
+
A modern, typed client for the official elTOQUE exchange-rate API, with sync and async interfaces.
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/eltoque-python/)
|
|
35
|
+
[](https://pypi.org/project/eltoque-python/)
|
|
36
|
+
[](https://github.com/Alejo2075/eltoque-python/actions/workflows/ci.yml)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
> **Independent project.** `eltoque-python` is community-maintained. It is not developed,
|
|
40
|
+
> sponsored, endorsed or supported by elTOQUE.
|
|
41
|
+
>
|
|
42
|
+
> **Exchange rates are indicative, not official. Source: elTOQUE.
|
|
43
|
+
> Access requires registration and an application-specific API token.**
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
from eltoque import ElToqueClient
|
|
49
|
+
|
|
50
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
51
|
+
response = client.get_latest_rates()
|
|
52
|
+
print(response.data) # the provider's JSON, untouched
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Leer en español: [docs/quickstart.es.md](docs/quickstart.es.md)
|
|
56
|
+
|
|
57
|
+
## Why use it?
|
|
58
|
+
|
|
59
|
+
- **Sync and async** clients with the same methods and behavior.
|
|
60
|
+
- **Typed** public API (`py.typed`, checked with `mypy --strict`).
|
|
61
|
+
- **Safe by default:** HTTPS only, explicit timeouts, no redirects followed (your token is never
|
|
62
|
+
forwarded to another host), bounded response size, and the token never appears in `repr`,
|
|
63
|
+
error messages or logs.
|
|
64
|
+
- **Predictable errors:** one exception family, one subclass per documented HTTP status.
|
|
65
|
+
- **Rate-limit aware:** parses `X-RateLimit-*` and `Retry-After`; retries are opt-in and bounded.
|
|
66
|
+
- **Pure Python**, one runtime dependency ([`httpx`](https://www.python-httpx.org/)), works in
|
|
67
|
+
plain scripts, notebooks, Django, Flask, FastAPI or AWS Lambda.
|
|
68
|
+
- **Tested offline** with mocked HTTP: no token needed to develop or run the test suite.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install eltoque-python
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Requires Python 3.11 or newer. To work on the SDK itself, install from a clone with `pip install -e .`.
|
|
77
|
+
|
|
78
|
+
## Get your API token
|
|
79
|
+
|
|
80
|
+
The SDK does not grant API access. Each application that integrates the API needs **its own
|
|
81
|
+
token**, requested from elTOQUE:
|
|
82
|
+
|
|
83
|
+
1. Fill in the official form: <https://tasas-token.eltoque.com/>
|
|
84
|
+
2. Read the [terms of use](https://eltoque.com/api-tasas-de-eltoque-terminos-y-condiciones-de-uso).
|
|
85
|
+
3. Keep the token secret and read it from the environment:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export ELTOQUE_API_TOKEN="your-application-token"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Never commit a token, share it with third parties, or ship it in a browser bundle.
|
|
92
|
+
|
|
93
|
+
## Quickstart
|
|
94
|
+
|
|
95
|
+
### Latest rates (rolling last 24 hours)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import os
|
|
99
|
+
|
|
100
|
+
from eltoque import ElToqueClient
|
|
101
|
+
|
|
102
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
103
|
+
response = client.get_latest_rates()
|
|
104
|
+
print(response.data)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### A specific window (shorter than 24 hours)
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import os
|
|
111
|
+
from datetime import datetime
|
|
112
|
+
|
|
113
|
+
from eltoque import ElToqueClient
|
|
114
|
+
|
|
115
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
116
|
+
response = client.get_rates(
|
|
117
|
+
datetime(2022, 10, 27, 0, 0, 0),
|
|
118
|
+
datetime(2022, 10, 27, 23, 59, 59),
|
|
119
|
+
)
|
|
120
|
+
print(response.data)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Async
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import asyncio
|
|
127
|
+
import os
|
|
128
|
+
|
|
129
|
+
from eltoque import AsyncElToqueClient
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def main() -> None:
|
|
133
|
+
async with AsyncElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
134
|
+
response = await client.get_latest_rates()
|
|
135
|
+
print(response.data)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Handling errors
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import os
|
|
145
|
+
|
|
146
|
+
from eltoque import ElToqueClient, ElToqueError, ElToqueRateLimitError
|
|
147
|
+
|
|
148
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"], max_retries=2) as client:
|
|
149
|
+
try:
|
|
150
|
+
response = client.get_latest_rates()
|
|
151
|
+
except ElToqueRateLimitError as error:
|
|
152
|
+
print(f"Rate limited, retry in {error.retry_after} s")
|
|
153
|
+
except ElToqueError as error:
|
|
154
|
+
print(f"Request failed: {error}")
|
|
155
|
+
else:
|
|
156
|
+
print(response.rate_limit.remaining, "requests left in this window")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## API reference
|
|
160
|
+
|
|
161
|
+
### Clients
|
|
162
|
+
|
|
163
|
+
`ElToqueClient` (sync) and `AsyncElToqueClient` (async, `await` every method) share this
|
|
164
|
+
constructor:
|
|
165
|
+
|
|
166
|
+
| Parameter | Default | Description |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| `api_token` | required | Your application token. Printable ASCII, no spaces, no `Bearer ` prefix. |
|
|
169
|
+
| `base_url` | `https://tasas.eltoque.com` | HTTPS only. Plain `http://` is accepted for `localhost`/loopback (tests). |
|
|
170
|
+
| `timeout` | `10 s` (`5 s` connect) | Seconds, or an `httpx.Timeout`. |
|
|
171
|
+
| `max_retries` | `0` | Opt-in retries (max 5). See [Rate limits](#rate-limits-and-retries). |
|
|
172
|
+
| `transport` | `None` | Test seam, e.g. `httpx.MockTransport`. |
|
|
173
|
+
| `http_client` | `None` | Your own `httpx.Client` / `httpx.AsyncClient`. It is never closed by the SDK, and its own timeout applies. |
|
|
174
|
+
|
|
175
|
+
| Method | Description |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `get_latest_rates()` | Rolling last 24 hours, computed by the provider at request time. |
|
|
178
|
+
| `get_rates(date_from, date_to)` | Explicit window, strictly shorter than 24 hours. |
|
|
179
|
+
| `get_rates_for_day(day)` | `day 00:00:00` to `day 23:59:59`. One request. |
|
|
180
|
+
| `close()` / `aclose()` | Release connections. Idempotent. Also called by `with` / `async with`. |
|
|
181
|
+
|
|
182
|
+
Using a client after it was closed raises `ElToqueError`.
|
|
183
|
+
|
|
184
|
+
### `RatesResponse`
|
|
185
|
+
|
|
186
|
+
| Attribute | Description |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `data` | The decoded JSON body, **exactly as the provider sent it** (object, list, scalar or `null`). |
|
|
189
|
+
| `status_code` | HTTP status (SDK metadata). |
|
|
190
|
+
| `rate_limit` | `RateLimitInfo` with optional `limit`, `remaining`, `reset`, `retry_after` (SDK metadata). |
|
|
191
|
+
| `received_at` | UTC timestamp of when the SDK received the response (SDK metadata). |
|
|
192
|
+
|
|
193
|
+
### Exceptions
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
ElToqueError
|
|
197
|
+
├── ElToqueConfigurationError invalid token, base_url, timeout, retries
|
|
198
|
+
├── ElToqueValidationError invalid arguments, detected before any request
|
|
199
|
+
├── ElToqueNetworkError connection, DNS, TLS, protocol failures
|
|
200
|
+
├── ElToqueTimeoutError
|
|
201
|
+
├── ElToqueDecodeError 200 response that is not valid JSON, or too large
|
|
202
|
+
└── ElToqueAPIError has .status_code, .body_excerpt, .rate_limit, .retry_after
|
|
203
|
+
├── ElToqueBadRequestError 400
|
|
204
|
+
├── ElToqueAuthenticationError 401
|
|
205
|
+
├── ElToqueUnprocessableError 422
|
|
206
|
+
├── ElToqueRateLimitError 429
|
|
207
|
+
└── ElToqueServerError 5xx
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Other 3xx/4xx statuses raise the base `ElToqueAPIError`. See [docs/errors.md](docs/errors.md).
|
|
211
|
+
|
|
212
|
+
## Date and time rules
|
|
213
|
+
|
|
214
|
+
- `date_from` and `date_to` must be **naive** `datetime` objects. Timezone-aware values are
|
|
215
|
+
rejected, because the API does not document its time zone and the SDK will not guess one.
|
|
216
|
+
- They are sent as `YYYY-MM-DD HH:MM:SS` (microseconds are dropped).
|
|
217
|
+
- `date_from` must be earlier than `date_to`, and the window must be **shorter than 24 hours**,
|
|
218
|
+
otherwise the API answers HTTP 400. The SDK rejects invalid windows before any request.
|
|
219
|
+
- `get_rates_for_day` is a convenience; it is not guaranteed to be a complete local calendar day.
|
|
220
|
+
|
|
221
|
+
## Rate limits and retries
|
|
222
|
+
|
|
223
|
+
By default each API key may make **60 requests/minute and up to 10 requests/second**; your key
|
|
224
|
+
may have different limits. Over the limit the API answers `429` with a `Retry-After` header.
|
|
225
|
+
|
|
226
|
+
- Retries are **off** (`max_retries=0`). Every retry consumes quota.
|
|
227
|
+
- With `max_retries > 0` the SDK retries only `429`, `502`, `503`, `504` and transport failures,
|
|
228
|
+
never `400`, `401` or `422`.
|
|
229
|
+
- `Retry-After` is honored. If the server asks for longer than 30 s (or the 60 s total budget),
|
|
230
|
+
the error is raised instead of waiting.
|
|
231
|
+
- There is no background polling, caching or bulk collection. You decide when to call.
|
|
232
|
+
|
|
233
|
+
## Known limitations
|
|
234
|
+
|
|
235
|
+
- Only one operation is documented by the provider: `GET /v1/trmi`. That is all this SDK calls.
|
|
236
|
+
- The provider **does not publish the schema of a successful response**, so `RatesResponse.data`
|
|
237
|
+
is untyped and no field (currency names, timestamps, ...) is guaranteed. Typed rate models will
|
|
238
|
+
come only after the schema is validated with authorized access.
|
|
239
|
+
- The time zone of the API is undocumented (see above).
|
|
240
|
+
- Installing this SDK does not give you access to the API; you need your own approved token.
|
|
241
|
+
|
|
242
|
+
## Supported Python versions
|
|
243
|
+
|
|
244
|
+
Python 3.11, 3.12, 3.13 and 3.14. No framework is required.
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
uv sync # or: pip install -e . && pip install pytest pytest-asyncio pytest-cov ruff mypy build twine
|
|
250
|
+
uv run ruff check .
|
|
251
|
+
uv run ruff format --check .
|
|
252
|
+
uv run mypy src
|
|
253
|
+
uv run pytest # offline, no token needed
|
|
254
|
+
uv run python -m build
|
|
255
|
+
uv run twine check dist/*
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Optional live check, only with your own authorized token (never run in CI):
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
ELTOQUE_API_TOKEN=... ELTOQUE_ALLOW_LIVE_TESTS=1 uv run pytest -m integration --no-cov
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Security and legal
|
|
265
|
+
|
|
266
|
+
- Treat your token like a password. See [docs/security.md](docs/security.md).
|
|
267
|
+
- This project is not affiliated with elTOQUE. When you display rates, name **elTOQUE as the
|
|
268
|
+
source** and describe them as **reference estimates**, as the
|
|
269
|
+
[terms of use](https://eltoque.com/api-tasas-de-eltoque-terminos-y-condiciones-de-uso) require.
|
|
270
|
+
Do not modify the data, share your key, resell the data or mass-extract it.
|
|
271
|
+
- The [MIT license](LICENSE) covers this SDK's source code only. It grants no rights over
|
|
272
|
+
elTOQUE's API, data, name or trademarks.
|
|
273
|
+
|
|
274
|
+
## Official resources
|
|
275
|
+
|
|
276
|
+
- API landing page: <https://tasas.eltoque.com/>
|
|
277
|
+
- Interactive docs: <https://tasas.eltoque.com/docs/>
|
|
278
|
+
- OpenAPI document: <https://tasas.eltoque.com/static/swagger.json>
|
|
279
|
+
- What is and is not verified: [docs/api-contract.md](docs/api-contract.md)
|
|
280
|
+
|
|
281
|
+
## Community
|
|
282
|
+
|
|
283
|
+
- Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
284
|
+
- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
285
|
+
- Security policy: [SECURITY.md](SECURITY.md)
|
|
286
|
+
- Release notes: [CHANGELOG.md](CHANGELOG.md)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# eltoque-python
|
|
2
|
+
|
|
3
|
+
**Unofficial Python SDK for elTOQUE exchange-rate data.**
|
|
4
|
+
A modern, typed client for the official elTOQUE exchange-rate API, with sync and async interfaces.
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/eltoque-python/)
|
|
7
|
+
[](https://pypi.org/project/eltoque-python/)
|
|
8
|
+
[](https://github.com/Alejo2075/eltoque-python/actions/workflows/ci.yml)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
> **Independent project.** `eltoque-python` is community-maintained. It is not developed,
|
|
12
|
+
> sponsored, endorsed or supported by elTOQUE.
|
|
13
|
+
>
|
|
14
|
+
> **Exchange rates are indicative, not official. Source: elTOQUE.
|
|
15
|
+
> Access requires registration and an application-specific API token.**
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
from eltoque import ElToqueClient
|
|
21
|
+
|
|
22
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
23
|
+
response = client.get_latest_rates()
|
|
24
|
+
print(response.data) # the provider's JSON, untouched
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Leer en español: [docs/quickstart.es.md](docs/quickstart.es.md)
|
|
28
|
+
|
|
29
|
+
## Why use it?
|
|
30
|
+
|
|
31
|
+
- **Sync and async** clients with the same methods and behavior.
|
|
32
|
+
- **Typed** public API (`py.typed`, checked with `mypy --strict`).
|
|
33
|
+
- **Safe by default:** HTTPS only, explicit timeouts, no redirects followed (your token is never
|
|
34
|
+
forwarded to another host), bounded response size, and the token never appears in `repr`,
|
|
35
|
+
error messages or logs.
|
|
36
|
+
- **Predictable errors:** one exception family, one subclass per documented HTTP status.
|
|
37
|
+
- **Rate-limit aware:** parses `X-RateLimit-*` and `Retry-After`; retries are opt-in and bounded.
|
|
38
|
+
- **Pure Python**, one runtime dependency ([`httpx`](https://www.python-httpx.org/)), works in
|
|
39
|
+
plain scripts, notebooks, Django, Flask, FastAPI or AWS Lambda.
|
|
40
|
+
- **Tested offline** with mocked HTTP: no token needed to develop or run the test suite.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install eltoque-python
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.11 or newer. To work on the SDK itself, install from a clone with `pip install -e .`.
|
|
49
|
+
|
|
50
|
+
## Get your API token
|
|
51
|
+
|
|
52
|
+
The SDK does not grant API access. Each application that integrates the API needs **its own
|
|
53
|
+
token**, requested from elTOQUE:
|
|
54
|
+
|
|
55
|
+
1. Fill in the official form: <https://tasas-token.eltoque.com/>
|
|
56
|
+
2. Read the [terms of use](https://eltoque.com/api-tasas-de-eltoque-terminos-y-condiciones-de-uso).
|
|
57
|
+
3. Keep the token secret and read it from the environment:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export ELTOQUE_API_TOKEN="your-application-token"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Never commit a token, share it with third parties, or ship it in a browser bundle.
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
### Latest rates (rolling last 24 hours)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import os
|
|
71
|
+
|
|
72
|
+
from eltoque import ElToqueClient
|
|
73
|
+
|
|
74
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
75
|
+
response = client.get_latest_rates()
|
|
76
|
+
print(response.data)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### A specific window (shorter than 24 hours)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import os
|
|
83
|
+
from datetime import datetime
|
|
84
|
+
|
|
85
|
+
from eltoque import ElToqueClient
|
|
86
|
+
|
|
87
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
88
|
+
response = client.get_rates(
|
|
89
|
+
datetime(2022, 10, 27, 0, 0, 0),
|
|
90
|
+
datetime(2022, 10, 27, 23, 59, 59),
|
|
91
|
+
)
|
|
92
|
+
print(response.data)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Async
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
import os
|
|
100
|
+
|
|
101
|
+
from eltoque import AsyncElToqueClient
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def main() -> None:
|
|
105
|
+
async with AsyncElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
106
|
+
response = await client.get_latest_rates()
|
|
107
|
+
print(response.data)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
asyncio.run(main())
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Handling errors
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import os
|
|
117
|
+
|
|
118
|
+
from eltoque import ElToqueClient, ElToqueError, ElToqueRateLimitError
|
|
119
|
+
|
|
120
|
+
with ElToqueClient(api_token=os.environ["ELTOQUE_API_TOKEN"], max_retries=2) as client:
|
|
121
|
+
try:
|
|
122
|
+
response = client.get_latest_rates()
|
|
123
|
+
except ElToqueRateLimitError as error:
|
|
124
|
+
print(f"Rate limited, retry in {error.retry_after} s")
|
|
125
|
+
except ElToqueError as error:
|
|
126
|
+
print(f"Request failed: {error}")
|
|
127
|
+
else:
|
|
128
|
+
print(response.rate_limit.remaining, "requests left in this window")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## API reference
|
|
132
|
+
|
|
133
|
+
### Clients
|
|
134
|
+
|
|
135
|
+
`ElToqueClient` (sync) and `AsyncElToqueClient` (async, `await` every method) share this
|
|
136
|
+
constructor:
|
|
137
|
+
|
|
138
|
+
| Parameter | Default | Description |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| `api_token` | required | Your application token. Printable ASCII, no spaces, no `Bearer ` prefix. |
|
|
141
|
+
| `base_url` | `https://tasas.eltoque.com` | HTTPS only. Plain `http://` is accepted for `localhost`/loopback (tests). |
|
|
142
|
+
| `timeout` | `10 s` (`5 s` connect) | Seconds, or an `httpx.Timeout`. |
|
|
143
|
+
| `max_retries` | `0` | Opt-in retries (max 5). See [Rate limits](#rate-limits-and-retries). |
|
|
144
|
+
| `transport` | `None` | Test seam, e.g. `httpx.MockTransport`. |
|
|
145
|
+
| `http_client` | `None` | Your own `httpx.Client` / `httpx.AsyncClient`. It is never closed by the SDK, and its own timeout applies. |
|
|
146
|
+
|
|
147
|
+
| Method | Description |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `get_latest_rates()` | Rolling last 24 hours, computed by the provider at request time. |
|
|
150
|
+
| `get_rates(date_from, date_to)` | Explicit window, strictly shorter than 24 hours. |
|
|
151
|
+
| `get_rates_for_day(day)` | `day 00:00:00` to `day 23:59:59`. One request. |
|
|
152
|
+
| `close()` / `aclose()` | Release connections. Idempotent. Also called by `with` / `async with`. |
|
|
153
|
+
|
|
154
|
+
Using a client after it was closed raises `ElToqueError`.
|
|
155
|
+
|
|
156
|
+
### `RatesResponse`
|
|
157
|
+
|
|
158
|
+
| Attribute | Description |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `data` | The decoded JSON body, **exactly as the provider sent it** (object, list, scalar or `null`). |
|
|
161
|
+
| `status_code` | HTTP status (SDK metadata). |
|
|
162
|
+
| `rate_limit` | `RateLimitInfo` with optional `limit`, `remaining`, `reset`, `retry_after` (SDK metadata). |
|
|
163
|
+
| `received_at` | UTC timestamp of when the SDK received the response (SDK metadata). |
|
|
164
|
+
|
|
165
|
+
### Exceptions
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
ElToqueError
|
|
169
|
+
├── ElToqueConfigurationError invalid token, base_url, timeout, retries
|
|
170
|
+
├── ElToqueValidationError invalid arguments, detected before any request
|
|
171
|
+
├── ElToqueNetworkError connection, DNS, TLS, protocol failures
|
|
172
|
+
├── ElToqueTimeoutError
|
|
173
|
+
├── ElToqueDecodeError 200 response that is not valid JSON, or too large
|
|
174
|
+
└── ElToqueAPIError has .status_code, .body_excerpt, .rate_limit, .retry_after
|
|
175
|
+
├── ElToqueBadRequestError 400
|
|
176
|
+
├── ElToqueAuthenticationError 401
|
|
177
|
+
├── ElToqueUnprocessableError 422
|
|
178
|
+
├── ElToqueRateLimitError 429
|
|
179
|
+
└── ElToqueServerError 5xx
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Other 3xx/4xx statuses raise the base `ElToqueAPIError`. See [docs/errors.md](docs/errors.md).
|
|
183
|
+
|
|
184
|
+
## Date and time rules
|
|
185
|
+
|
|
186
|
+
- `date_from` and `date_to` must be **naive** `datetime` objects. Timezone-aware values are
|
|
187
|
+
rejected, because the API does not document its time zone and the SDK will not guess one.
|
|
188
|
+
- They are sent as `YYYY-MM-DD HH:MM:SS` (microseconds are dropped).
|
|
189
|
+
- `date_from` must be earlier than `date_to`, and the window must be **shorter than 24 hours**,
|
|
190
|
+
otherwise the API answers HTTP 400. The SDK rejects invalid windows before any request.
|
|
191
|
+
- `get_rates_for_day` is a convenience; it is not guaranteed to be a complete local calendar day.
|
|
192
|
+
|
|
193
|
+
## Rate limits and retries
|
|
194
|
+
|
|
195
|
+
By default each API key may make **60 requests/minute and up to 10 requests/second**; your key
|
|
196
|
+
may have different limits. Over the limit the API answers `429` with a `Retry-After` header.
|
|
197
|
+
|
|
198
|
+
- Retries are **off** (`max_retries=0`). Every retry consumes quota.
|
|
199
|
+
- With `max_retries > 0` the SDK retries only `429`, `502`, `503`, `504` and transport failures,
|
|
200
|
+
never `400`, `401` or `422`.
|
|
201
|
+
- `Retry-After` is honored. If the server asks for longer than 30 s (or the 60 s total budget),
|
|
202
|
+
the error is raised instead of waiting.
|
|
203
|
+
- There is no background polling, caching or bulk collection. You decide when to call.
|
|
204
|
+
|
|
205
|
+
## Known limitations
|
|
206
|
+
|
|
207
|
+
- Only one operation is documented by the provider: `GET /v1/trmi`. That is all this SDK calls.
|
|
208
|
+
- The provider **does not publish the schema of a successful response**, so `RatesResponse.data`
|
|
209
|
+
is untyped and no field (currency names, timestamps, ...) is guaranteed. Typed rate models will
|
|
210
|
+
come only after the schema is validated with authorized access.
|
|
211
|
+
- The time zone of the API is undocumented (see above).
|
|
212
|
+
- Installing this SDK does not give you access to the API; you need your own approved token.
|
|
213
|
+
|
|
214
|
+
## Supported Python versions
|
|
215
|
+
|
|
216
|
+
Python 3.11, 3.12, 3.13 and 3.14. No framework is required.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
uv sync # or: pip install -e . && pip install pytest pytest-asyncio pytest-cov ruff mypy build twine
|
|
222
|
+
uv run ruff check .
|
|
223
|
+
uv run ruff format --check .
|
|
224
|
+
uv run mypy src
|
|
225
|
+
uv run pytest # offline, no token needed
|
|
226
|
+
uv run python -m build
|
|
227
|
+
uv run twine check dist/*
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Optional live check, only with your own authorized token (never run in CI):
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
ELTOQUE_API_TOKEN=... ELTOQUE_ALLOW_LIVE_TESTS=1 uv run pytest -m integration --no-cov
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Security and legal
|
|
237
|
+
|
|
238
|
+
- Treat your token like a password. See [docs/security.md](docs/security.md).
|
|
239
|
+
- This project is not affiliated with elTOQUE. When you display rates, name **elTOQUE as the
|
|
240
|
+
source** and describe them as **reference estimates**, as the
|
|
241
|
+
[terms of use](https://eltoque.com/api-tasas-de-eltoque-terminos-y-condiciones-de-uso) require.
|
|
242
|
+
Do not modify the data, share your key, resell the data or mass-extract it.
|
|
243
|
+
- The [MIT license](LICENSE) covers this SDK's source code only. It grants no rights over
|
|
244
|
+
elTOQUE's API, data, name or trademarks.
|
|
245
|
+
|
|
246
|
+
## Official resources
|
|
247
|
+
|
|
248
|
+
- API landing page: <https://tasas.eltoque.com/>
|
|
249
|
+
- Interactive docs: <https://tasas.eltoque.com/docs/>
|
|
250
|
+
- OpenAPI document: <https://tasas.eltoque.com/static/swagger.json>
|
|
251
|
+
- What is and is not verified: [docs/api-contract.md](docs/api-contract.md)
|
|
252
|
+
|
|
253
|
+
## Community
|
|
254
|
+
|
|
255
|
+
- Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
256
|
+
- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
257
|
+
- Security policy: [SECURITY.md](SECURITY.md)
|
|
258
|
+
- Release notes: [CHANGELOG.md](CHANGELOG.md)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# elTOQUE API contract
|
|
2
|
+
|
|
3
|
+
Last validated against the official sources: **2026-10-22** (no token used; only the public
|
|
4
|
+
OpenAPI document was fetched).
|
|
5
|
+
|
|
6
|
+
## Sources
|
|
7
|
+
|
|
8
|
+
- Landing page: <https://tasas.eltoque.com/>
|
|
9
|
+
- Interactive docs: <https://tasas.eltoque.com/docs/>
|
|
10
|
+
- OpenAPI JSON: <https://tasas.eltoque.com/static/swagger.json>
|
|
11
|
+
- API key form: <https://tasas-token.eltoque.com/>
|
|
12
|
+
- Terms of use: <https://eltoque.com/api-tasas-de-eltoque-terminos-y-condiciones-de-uso>
|
|
13
|
+
|
|
14
|
+
## Verified (from the published OpenAPI document)
|
|
15
|
+
|
|
16
|
+
| Item | Value |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Origin | `https://tasas.eltoque.com` |
|
|
19
|
+
| OpenAPI / API version | `3.0.0` / `1.0.beta` (not the SDK version) |
|
|
20
|
+
| Operations | 1: `GET /v1/trmi` (`trmiV1`) |
|
|
21
|
+
| Auth | HTTP bearer (`Authorization: Bearer <token>`), JWT |
|
|
22
|
+
| Query | `date_from`, `date_to`, optional, example `2022-10-27 00:00:01` |
|
|
23
|
+
| No dates | Rolling last 24 hours from request time |
|
|
24
|
+
| Explicit range | Must be less than 24 hours, otherwise HTTP 400 |
|
|
25
|
+
| Documented errors | 400, 401, 422, 429 |
|
|
26
|
+
| Default limits | 60 requests/minute and 10 requests/second, per API key |
|
|
27
|
+
| Rate-limit headers | `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on all responses |
|
|
28
|
+
| 429 | `Retry-After` (seconds) and JSON `{"error": "...", "limit": "60 per 1 minute"}` |
|
|
29
|
+
|
|
30
|
+
Terms highlights: name elTOQUE as the source, rates are referential, do not share your key,
|
|
31
|
+
do not modify the data (except to update it), one token per integrated application.
|
|
32
|
+
|
|
33
|
+
## Unverified / unknown
|
|
34
|
+
|
|
35
|
+
- [ ] Schema of the HTTP 200 body (the OpenAPI document defines none).
|
|
36
|
+
- [ ] Currencies, field names, units, precision.
|
|
37
|
+
- [ ] Time zone of `date_from` / `date_to` and of returned timestamps.
|
|
38
|
+
- [ ] Behaviour when only one of `date_from` / `date_to` is sent.
|
|
39
|
+
- [ ] Behaviour of missing data for a requested window.
|
|
40
|
+
- [ ] Exact semantics of `X-RateLimit-Reset` (epoch vs. seconds remaining).
|
|
41
|
+
|
|
42
|
+
Consequence: `RatesResponse.data` returns the decoded JSON untouched. Typed rate models are
|
|
43
|
+
deferred until the 200 schema is validated with an authorized token (Milestone B).
|
|
44
|
+
|
|
45
|
+
## Change log of the contract
|
|
46
|
+
|
|
47
|
+
| Date | Change |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| 2026-10-22 | Initial validation; matches `CLAUDE.md` section 2. |
|