quantpad-data 0.7.0__tar.gz → 0.7.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 (24) hide show
  1. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/PKG-INFO +1 -1
  2. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/pyproject.toml +1 -1
  3. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/client.py +29 -2
  4. quantpad_data-0.7.1/tests/test_refusal_hint.py +71 -0
  5. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/.gitignore +0 -0
  6. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/README.md +0 -0
  7. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/__init__.py +0 -0
  8. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/cache.py +0 -0
  9. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/errors.py +0 -0
  10. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/lean.py +0 -0
  11. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/memory.py +0 -0
  12. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/py.typed +0 -0
  13. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/rth.py +0 -0
  14. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/v2.py +0 -0
  15. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/conftest.py +0 -0
  16. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_client.py +0 -0
  17. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_lean.py +0 -0
  18. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_message_encoding.py +0 -0
  19. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_packaging.py +0 -0
  20. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_rth.py +0 -0
  21. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_v2.py +0 -0
  22. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_v2_cache.py +0 -0
  23. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_v2_gate.py +0 -0
  24. {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quantpad-data
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Official Python client for the QuantPad market-data API
5
5
  Project-URL: Documentation, https://api.quantpad.ai/external/docs
6
6
  Author: QuantPad
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quantpad-data"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Official Python client for the QuantPad market-data API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -84,7 +84,7 @@ V2_NOT_AVAILABLE = (
84
84
  "QuantPad support about /v2 access."
85
85
  )
86
86
 
87
- VERSION = "0.7.0"
87
+ VERSION = "0.7.1"
88
88
  USER_AGENT = f"quantpad-data-python/{VERSION}"
89
89
  TICK_SCHEMAS = (
90
90
  "trades", "mbp-1", "mbp-10", "cmbp-1", "tcbbo", "cbbo-1s",
@@ -143,6 +143,25 @@ BARS_TIMEOUT = (30, 1800)
143
143
  V2_DATA_TIMEOUT = (10, 1800)
144
144
 
145
145
 
146
+ def _prose_with_hint(error: dict) -> str:
147
+ """The sentence to raise from a structured refusal.
148
+
149
+ ``message`` first; a ``hint`` follows it as a second sentence when the
150
+ service sent one. Kept out of the message on the wire so a caller
151
+ reading the body can still tell the refusal from the advice, and put
152
+ back here because the exception text is the only thing most callers
153
+ ever see. A body with a hint and no message is stringified whole, as
154
+ it always was.
155
+ """
156
+ message = error.get("message")
157
+ if not message:
158
+ return str(error)
159
+ hint = error.get("hint")
160
+ if isinstance(hint, str) and hint.strip():
161
+ return f"{message} {hint.strip()}"
162
+ return str(message)
163
+
164
+
146
165
  class Client:
147
166
  def __init__(
148
167
  self,
@@ -189,6 +208,14 @@ class Client:
189
208
  user where the service had written a sentence. So the prose is
190
209
  unwrapped from ``message`` when there is one, and the object is
191
210
  kept beside it for the callers that need to branch on it.
211
+
212
+ A ``hint`` rides with the prose. The /v2 404 for a symbol nothing
213
+ resolves carries what to send instead -- ``NQ.FUT`` with
214
+ ``stype_in='parent'`` for a bare ``NQ`` -- as its own field so a
215
+ caller branching on the body can tell it from the message; but the
216
+ exception a notebook prints is the message, and advice a client
217
+ drops on the floor is advice nobody gets (the shape
218
+ ``options.chain`` had with ``truncated``).
192
219
  """
193
220
  try:
194
221
  body = response.json()
@@ -198,7 +225,7 @@ class Client:
198
225
  return str(body), None
199
226
  error = body.get("error") or body.get("detail") or body
200
227
  if isinstance(error, dict):
201
- return str(error.get("message") or error), error
228
+ return _prose_with_hint(error), error
202
229
  return str(error), None
203
230
 
204
231
  @classmethod
@@ -0,0 +1,71 @@
1
+ """A structured refusal's ``hint`` reaches the exception text.
2
+
3
+ The /v2 404 for a symbol nothing resolves carries what to send instead as
4
+ its own ``hint`` field -- ``NQ.FUT with stype_in='parent'`` for a bare
5
+ ``NQ`` -- so a caller reading the body can tell the refusal from the
6
+ advice. The client used to raise ``message`` alone, which is the shape
7
+ ``options.chain`` had with ``truncated``: the server said the useful thing
8
+ and the SDK dropped it before anyone could read it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+
15
+ import pytest
16
+ import requests
17
+
18
+ from quantpad_data.client import Client
19
+ from quantpad_data.errors import NotFoundError
20
+
21
+ MESSAGE = "None of the requested symbols resolve to an instrument on GLBX.MDP3 over this window."
22
+ HINT = "For NQ try NQ.FUT with stype_in='parent' (every contract in the chain)."
23
+
24
+
25
+ def _response(body: dict, status: int = 404) -> requests.Response:
26
+ response = requests.Response()
27
+ response.status_code = status
28
+ response.url = "https://example.test/v2/timeseries/get_range"
29
+ response._content = json.dumps({"error": body}).encode()
30
+ response.headers["Content-Type"] = "application/json"
31
+ return response
32
+
33
+
34
+ def _client() -> Client:
35
+ return Client(api_key="k", base_url="https://example.test")
36
+
37
+
38
+ class TestTheHintIsRaisedWithTheMessage:
39
+ def test_the_exception_text_carries_both_sentences_in_order(self):
40
+ with pytest.raises(NotFoundError) as caught:
41
+ _client()._raise_for_status(
42
+ _response({"message": MESSAGE, "hint": HINT, "not_found": ["NQ"]}),
43
+ "/v2/timeseries/get_range",
44
+ )
45
+ text = str(caught.value)
46
+ assert text == f"{MESSAGE} {HINT}"
47
+
48
+ def test_the_structured_body_is_still_available_beside_it(self):
49
+ """Branching callers read the fields; the hint joining the prose
50
+ must not take the fields away."""
51
+ message, detail = Client._envelope(
52
+ _response({"message": MESSAGE, "hint": HINT, "not_found": ["NQ"]})
53
+ )
54
+ assert detail["hint"] == HINT
55
+ assert detail["not_found"] == ["NQ"]
56
+ assert message.startswith(MESSAGE)
57
+
58
+ def test_a_refusal_without_a_hint_reads_exactly_as_before(self):
59
+ message, _ = Client._envelope(_response({"message": MESSAGE}))
60
+ assert message == MESSAGE
61
+
62
+ @pytest.mark.parametrize("hint", ["", " ", None, 7, ["not", "prose"]])
63
+ def test_a_blank_or_non_prose_hint_adds_nothing(self, hint):
64
+ message, _ = Client._envelope(_response({"message": MESSAGE, "hint": hint}))
65
+ assert message == MESSAGE
66
+
67
+ def test_a_hint_with_no_message_does_not_become_the_message(self):
68
+ """A body with advice but no refusal is malformed; stringify it
69
+ whole as the client always has rather than promote the hint."""
70
+ message, _ = Client._envelope(_response({"hint": HINT}))
71
+ assert message == str({"hint": HINT})
File without changes
File without changes