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.
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/PKG-INFO +1 -1
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/pyproject.toml +1 -1
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/client.py +29 -2
- quantpad_data-0.7.1/tests/test_refusal_hint.py +71 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/.gitignore +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/README.md +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/__init__.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/cache.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/errors.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/lean.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/memory.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/py.typed +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/rth.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/quantpad_data/v2.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/conftest.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_client.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_lean.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_message_encoding.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_packaging.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_rth.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_v2.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_v2_cache.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_v2_gate.py +0 -0
- {quantpad_data-0.7.0 → quantpad_data-0.7.1}/tests/test_version.py +0 -0
|
@@ -84,7 +84,7 @@ V2_NOT_AVAILABLE = (
|
|
|
84
84
|
"QuantPad support about /v2 access."
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
VERSION = "0.7.
|
|
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
|
|
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
|
|
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
|