eltoque-python 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/CHANGELOG.md +13 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/PKG-INFO +8 -4
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/README.md +7 -3
- eltoque_python-0.1.1/docs/live-testing.md +70 -0
- eltoque_python-0.1.1/src/eltoque/_version.py +1 -0
- eltoque_python-0.1.1/tests/test_live_check.py +86 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_public_api.py +3 -1
- eltoque_python-0.1.0/src/eltoque/_version.py +0 -1
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/.gitignore +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/LICENSE +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/api-contract.md +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/configuration.md +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/errors.md +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/publishing.md +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/quickstart.es.md +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/security.md +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/examples/async_basic.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/examples/basic.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/examples/date_range.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/pyproject.toml +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/__init__.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_async_client.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_client.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_config.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_dates.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_retry.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_transport.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/exceptions.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/models.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/py.typed +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/__init__.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/conftest.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_async_client.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_client.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_concurrency.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_config.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_dates.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_docs.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_errors.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_failures.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_integration.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_models.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_parity.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_rate_limits.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_retry.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_security.py +0 -0
- {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_transport.py +0 -0
|
@@ -8,6 +8,19 @@ version (`1.0.beta`).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.1] - 2026-10-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `scripts/live_check.py`: opt-in single-request check against the real API that prints only the
|
|
16
|
+
response shape (never provider values), plus `docs/live-testing.md` with the validation
|
|
17
|
+
checklist for the first authorized run.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- README header now shows the project logo (absolute URL, so it also renders on PyPI) and
|
|
22
|
+
documents `pip install eltoque-python`.
|
|
23
|
+
|
|
11
24
|
## [0.1.0] - 2026-10-22
|
|
12
25
|
|
|
13
26
|
First release. Unpublished until the repository owner approves it (see docs/publishing.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: eltoque-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Unofficial, typed Python client for the official elTOQUE exchange-rate API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Alejo2075/eltoque-python
|
|
6
6
|
Project-URL: Source, https://github.com/Alejo2075/eltoque-python
|
|
@@ -26,7 +26,9 @@ Requires-Python: >=3.11
|
|
|
26
26
|
Requires-Dist: httpx<1,>=0.28
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="https://raw.githubusercontent.com/Alejo2075/eltoque-python/main/assets/logo.png" alt="eltoque-python" width="480">
|
|
31
|
+
</p>
|
|
30
32
|
|
|
31
33
|
**Unofficial Python SDK for elTOQUE exchange-rate data.**
|
|
32
34
|
A modern, typed client for the official elTOQUE exchange-rate API, with sync and async interfaces.
|
|
@@ -255,10 +257,12 @@ uv run python -m build
|
|
|
255
257
|
uv run twine check dist/*
|
|
256
258
|
```
|
|
257
259
|
|
|
258
|
-
Optional live check, only with your own authorized token (never run in CI)
|
|
260
|
+
Optional live check, only with your own authorized token (never run in CI). One request; the
|
|
261
|
+
script prints the response shape, never provider values. See
|
|
262
|
+
[docs/live-testing.md](docs/live-testing.md):
|
|
259
263
|
|
|
260
264
|
```bash
|
|
261
|
-
ELTOQUE_API_TOKEN=... ELTOQUE_ALLOW_LIVE_TESTS=1
|
|
265
|
+
ELTOQUE_API_TOKEN=... ELTOQUE_ALLOW_LIVE_TESTS=1 python scripts/live_check.py
|
|
262
266
|
```
|
|
263
267
|
|
|
264
268
|
## Security and legal
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Alejo2075/eltoque-python/main/assets/logo.png" alt="eltoque-python" width="480">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
5
|
**Unofficial Python SDK for elTOQUE exchange-rate data.**
|
|
4
6
|
A modern, typed client for the official elTOQUE exchange-rate API, with sync and async interfaces.
|
|
@@ -227,10 +229,12 @@ uv run python -m build
|
|
|
227
229
|
uv run twine check dist/*
|
|
228
230
|
```
|
|
229
231
|
|
|
230
|
-
Optional live check, only with your own authorized token (never run in CI)
|
|
232
|
+
Optional live check, only with your own authorized token (never run in CI). One request; the
|
|
233
|
+
script prints the response shape, never provider values. See
|
|
234
|
+
[docs/live-testing.md](docs/live-testing.md):
|
|
231
235
|
|
|
232
236
|
```bash
|
|
233
|
-
ELTOQUE_API_TOKEN=... ELTOQUE_ALLOW_LIVE_TESTS=1
|
|
237
|
+
ELTOQUE_API_TOKEN=... ELTOQUE_ALLOW_LIVE_TESTS=1 python scripts/live_check.py
|
|
234
238
|
```
|
|
235
239
|
|
|
236
240
|
## Security and legal
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Validating against the real API
|
|
2
|
+
|
|
3
|
+
The test suite never touches the real elTOQUE API. This page is the checklist for the first time
|
|
4
|
+
you have **your own approved token** (request it at <https://tasas-token.eltoque.com/>). It is
|
|
5
|
+
meant to close the items listed as unverified in [api-contract.md](api-contract.md).
|
|
6
|
+
|
|
7
|
+
Ground rules: use only your own application's token, keep the number of requests tiny (default
|
|
8
|
+
limit: 60/minute, 10/second), never commit tokens or real responses, and do not redistribute
|
|
9
|
+
the data (see the [terms of use](https://eltoque.com/api-tasas-de-eltoque-terminos-y-condiciones-de-uso)).
|
|
10
|
+
|
|
11
|
+
## 1. Smoke check: does the client work? (1 request)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export ELTOQUE_API_TOKEN="your-application-token"
|
|
15
|
+
export ELTOQUE_ALLOW_LIVE_TESTS=1
|
|
16
|
+
python scripts/live_check.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It refuses to run unless both variables are set, makes **one** request and prints:
|
|
20
|
+
|
|
21
|
+
- the HTTP status and rate-limit headers (`limit`, `remaining`, `reset`);
|
|
22
|
+
- the **shape** of the JSON (types, keys, list sizes, decimal places) and **no values**.
|
|
23
|
+
|
|
24
|
+
Exit codes: `0` ok, `1` the API or network failed (error class and message shown), `2` not
|
|
25
|
+
authorized to run.
|
|
26
|
+
|
|
27
|
+
Or run the opt-in pytest check (also one request): `uv run pytest -m integration --no-cov`.
|
|
28
|
+
|
|
29
|
+
## 2. Record what you learn
|
|
30
|
+
|
|
31
|
+
From the shape output, fill in the "Unverified / unknown" checklist in
|
|
32
|
+
[api-contract.md](api-contract.md) with a validation date:
|
|
33
|
+
|
|
34
|
+
- [ ] Top-level type (object or list) and field names; which are currencies.
|
|
35
|
+
- [ ] Types and decimal places of the rate values (decides `Decimal` vs. `float`).
|
|
36
|
+
- [ ] Whether there are timestamps, and in which format.
|
|
37
|
+
- [ ] What `X-RateLimit-Reset` means (compare it with the current time on two consecutive calls).
|
|
38
|
+
|
|
39
|
+
Paste only the value-free shape. Review it first: object keys are printed as-is.
|
|
40
|
+
|
|
41
|
+
## 3. Time zone and ranges (2-3 more requests, done by hand)
|
|
42
|
+
|
|
43
|
+
The API does not document its time zone. With a short Python session:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from datetime import datetime, timedelta
|
|
48
|
+
|
|
49
|
+
from eltoque import ElToqueClient
|
|
50
|
+
|
|
51
|
+
with ElToqueClient(os.environ["ELTOQUE_API_TOKEN"]) as client:
|
|
52
|
+
now = datetime.now() # your local, naive time
|
|
53
|
+
r1 = client.get_rates(now - timedelta(hours=2), now - timedelta(hours=1))
|
|
54
|
+
print(type(r1.data), r1.rate_limit)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Compare windows shifted by the suspected offset and see which returns data for the period you
|
|
58
|
+
expect. Also confirm what happens with a window of exactly 24 hours (expected: HTTP 400, which the
|
|
59
|
+
SDK already rejects before sending) and with only one bound (the SDK refuses this today).
|
|
60
|
+
|
|
61
|
+
## 4. Turn findings into code (Milestone B, release 0.2.0)
|
|
62
|
+
|
|
63
|
+
1. Update `docs/api-contract.md` with the sanitized shape and the date.
|
|
64
|
+
2. Add `Decimal`-based typed models **only for confirmed fields**, keeping `RatesResponse.data` as
|
|
65
|
+
the untouched escape hatch. Use synthetic fixtures that mimic the confirmed shape.
|
|
66
|
+
3. If the time zone is confirmed, document it in the README and decide whether timezone-aware
|
|
67
|
+
datetimes can be supported.
|
|
68
|
+
4. Tests, changelog (`Added`), version bump, then follow [publishing.md](publishing.md).
|
|
69
|
+
|
|
70
|
+
You can ask Claude Code to do step 4 from your sanitized notes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""The live-check helper must be inert without authorization and never leak provider values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "live_check.py"
|
|
12
|
+
spec = importlib.util.spec_from_file_location("live_check", SCRIPT)
|
|
13
|
+
assert spec is not None
|
|
14
|
+
assert spec.loader is not None
|
|
15
|
+
live_check = importlib.util.module_from_spec(spec)
|
|
16
|
+
spec.loader.exec_module(live_check)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize(
|
|
20
|
+
"env",
|
|
21
|
+
[{}, {"ELTOQUE_API_TOKEN": "fake.token"}, {"ELTOQUE_ALLOW_LIVE_TESTS": "1"}],
|
|
22
|
+
)
|
|
23
|
+
def test_refuses_without_both_variables(
|
|
24
|
+
env: dict[str, str], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
25
|
+
) -> None:
|
|
26
|
+
monkeypatch.delenv("ELTOQUE_API_TOKEN", raising=False)
|
|
27
|
+
monkeypatch.delenv("ELTOQUE_ALLOW_LIVE_TESTS", raising=False)
|
|
28
|
+
for key, value in env.items():
|
|
29
|
+
monkeypatch.setenv(key, value)
|
|
30
|
+
|
|
31
|
+
def boom(*args: object, **kwargs: object) -> None:
|
|
32
|
+
raise AssertionError("no network allowed")
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr(httpx.HTTPTransport, "handle_request", boom)
|
|
35
|
+
assert live_check.main() == 2
|
|
36
|
+
assert "Refusing" in capsys.readouterr().err
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_prints_shape_but_never_values(
|
|
40
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
41
|
+
) -> None:
|
|
42
|
+
monkeypatch.setenv("ELTOQUE_API_TOKEN", "fake.token")
|
|
43
|
+
monkeypatch.setenv("ELTOQUE_ALLOW_LIVE_TESTS", "1")
|
|
44
|
+
payload = {"rates": [{"code": "SECRETVALUE", "price": 365.25}], "n": 3, "ok": True, "x": None}
|
|
45
|
+
calls = []
|
|
46
|
+
|
|
47
|
+
def handle(self: httpx.HTTPTransport, request: httpx.Request) -> httpx.Response:
|
|
48
|
+
calls.append(request)
|
|
49
|
+
return httpx.Response(200, json=payload, headers={"X-RateLimit-Remaining": "59"})
|
|
50
|
+
|
|
51
|
+
monkeypatch.setattr(httpx.HTTPTransport, "handle_request", handle)
|
|
52
|
+
assert live_check.main() == 0
|
|
53
|
+
out = capsys.readouterr().out
|
|
54
|
+
assert len(calls) == 1
|
|
55
|
+
assert "SECRETVALUE" not in out
|
|
56
|
+
assert "365.25" not in out
|
|
57
|
+
assert "fake.token" not in out
|
|
58
|
+
assert "float(decimals=2)" in out
|
|
59
|
+
assert '"remaining": 59' in out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_api_error_exits_nonzero_without_leaking(
|
|
63
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
64
|
+
) -> None:
|
|
65
|
+
monkeypatch.setenv("ELTOQUE_API_TOKEN", "fake.token")
|
|
66
|
+
monkeypatch.setenv("ELTOQUE_ALLOW_LIVE_TESTS", "1")
|
|
67
|
+
monkeypatch.setattr(
|
|
68
|
+
httpx.HTTPTransport,
|
|
69
|
+
"handle_request",
|
|
70
|
+
lambda self, request: httpx.Response(401, text="fake.token rejected"),
|
|
71
|
+
)
|
|
72
|
+
assert live_check.main() == 1
|
|
73
|
+
captured = capsys.readouterr()
|
|
74
|
+
assert "ElToqueAuthenticationError" in captured.err
|
|
75
|
+
assert "fake.token" not in captured.err + captured.out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_shape_limits_depth_and_size() -> None:
|
|
79
|
+
deep: object = 1
|
|
80
|
+
for _ in range(20):
|
|
81
|
+
deep = [deep]
|
|
82
|
+
assert "<max depth>" in str(live_check.describe_shape(deep))
|
|
83
|
+
big = {f"k{i}": i for i in range(100)}
|
|
84
|
+
assert live_check.describe_shape(big)["<more keys>"] == 100 - live_check.MAX_ITEMS
|
|
85
|
+
assert live_check.describe_shape(1e-7) == "float(decimals=exp)"
|
|
86
|
+
assert live_check.describe_shape(object()) == "object"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|