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.
Files changed (47) hide show
  1. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/CHANGELOG.md +13 -0
  2. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/PKG-INFO +8 -4
  3. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/README.md +7 -3
  4. eltoque_python-0.1.1/docs/live-testing.md +70 -0
  5. eltoque_python-0.1.1/src/eltoque/_version.py +1 -0
  6. eltoque_python-0.1.1/tests/test_live_check.py +86 -0
  7. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_public_api.py +3 -1
  8. eltoque_python-0.1.0/src/eltoque/_version.py +0 -1
  9. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/.gitignore +0 -0
  10. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/LICENSE +0 -0
  11. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/api-contract.md +0 -0
  12. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/configuration.md +0 -0
  13. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/errors.md +0 -0
  14. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/publishing.md +0 -0
  15. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/quickstart.es.md +0 -0
  16. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/docs/security.md +0 -0
  17. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/examples/async_basic.py +0 -0
  18. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/examples/basic.py +0 -0
  19. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/examples/date_range.py +0 -0
  20. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/pyproject.toml +0 -0
  21. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/__init__.py +0 -0
  22. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_async_client.py +0 -0
  23. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_client.py +0 -0
  24. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_config.py +0 -0
  25. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_dates.py +0 -0
  26. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_retry.py +0 -0
  27. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/_transport.py +0 -0
  28. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/exceptions.py +0 -0
  29. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/models.py +0 -0
  30. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/src/eltoque/py.typed +0 -0
  31. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/__init__.py +0 -0
  32. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/conftest.py +0 -0
  33. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_async_client.py +0 -0
  34. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_client.py +0 -0
  35. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_concurrency.py +0 -0
  36. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_config.py +0 -0
  37. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_dates.py +0 -0
  38. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_docs.py +0 -0
  39. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_errors.py +0 -0
  40. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_failures.py +0 -0
  41. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_integration.py +0 -0
  42. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_models.py +0 -0
  43. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_parity.py +0 -0
  44. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_rate_limits.py +0 -0
  45. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_retry.py +0 -0
  46. {eltoque_python-0.1.0 → eltoque_python-0.1.1}/tests/test_security.py +0 -0
  47. {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.0
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
- # eltoque-python
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 uv run pytest -m integration --no-cov
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
- # eltoque-python
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 uv run pytest -m integration --no-cov
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,8 +1,10 @@
1
+ import re
2
+
1
3
  import eltoque
2
4
 
3
5
 
4
6
  def test_version_is_exposed() -> None:
5
- assert eltoque.__version__ == "0.1.0"
7
+ assert re.fullmatch(r"\d+\.\d+\.\d+", eltoque.__version__)
6
8
 
7
9
 
8
10
  def test_all_names_resolve() -> None:
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes