quantpad-data 0.4.1__tar.gz → 0.4.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quantpad-data
3
- Version: 0.4.1
3
+ Version: 0.4.2
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.4.1"
7
+ version = "0.4.2"
8
8
  description = "Official Python client for the QuantPad market-data API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -90,7 +90,7 @@ V2_NOT_AVAILABLE = (
90
90
  "QuantPad support about /v2 access."
91
91
  )
92
92
 
93
- VERSION = "0.4.1"
93
+ VERSION = "0.4.2"
94
94
  USER_AGENT = f"quantpad-data-python/{VERSION}"
95
95
  TICK_SCHEMAS = (
96
96
  "trades", "mbp-1", "mbp-10", "cmbp-1", "tcbbo", "cbbo-1s",
@@ -849,11 +849,20 @@ def _merged_manifest(payloads: Sequence[dict]) -> dict:
849
849
  # symbol the second piece resolved was served, and listing it as
850
850
  # not found would report an answer the caller is holding as absent.
851
851
  symbols = {s for r in resolutions for s in (r.get("symbols") or ())}
852
- found = symbols | set(instruments)
852
+ # ``found`` is what RESOLVED and nothing else. Reading ``symbols``
853
+ # into it would be safe against today's server, where the field is
854
+ # derived from the same intervals as ``resolved`` -- but its NAME says
855
+ # "what was asked for", and if it ever carries a requested handle that
856
+ # resolved nowhere, folding it in here deletes that handle from
857
+ # ``not_found`` and a complete MISS reads as served. On every read,
858
+ # not only a split. The whole point of this function is to stop a
859
+ # label claiming more than was served, so it must not depend on two
860
+ # fields of one echo agreeing.
861
+ found = set(instruments)
853
862
  merged["resolved"] = {
854
863
  **resolutions[0],
855
864
  "resolved": instruments,
856
- "symbols": sorted(found),
865
+ "symbols": sorted(symbols | found),
857
866
  "not_found": [
858
867
  symbol
859
868
  for symbol in dict.fromkeys(
@@ -662,6 +662,58 @@ class TestAWindowTooColdToFetchInOneGo:
662
662
  # the caller is holding missing.
663
663
  assert resolution["not_found"] == []
664
664
 
665
+ def test_a_symbol_no_piece_resolved_stays_not_found(self, monkeypatch):
666
+ """The other direction, and the one that fails dangerously.
667
+
668
+ ``not_found`` is emptied by what RESOLVED, so it must not be emptied
669
+ by what was ASKED FOR. Against today's server the two cannot
670
+ disagree -- ``symbols`` is derived from the same intervals as
671
+ ``resolved`` -- but the field is *named* for the request, and if it
672
+ ever becomes that, folding it into the found set would delete a
673
+ genuine miss and a symbol nobody served would read as served.
674
+ """
675
+ def piece(url, day, resolved):
676
+ return json_response({
677
+ "format": "parquet",
678
+ "files": [{"day": day, "instrument_id": 42, "symbol": "NVDA",
679
+ "key": "k", "size_bytes": 10, "url": url}],
680
+ "gaps": [],
681
+ "metadata": {"dataset": "XNAS.ITCH", "schema": "mbp-10",
682
+ "served_start": "2026-08-20",
683
+ "served_end": "2026-09-17",
684
+ "resolved": resolved},
685
+ })
686
+
687
+ # WHAT WAS ASKED FOR, which is the reading the field's name invites:
688
+ # both symbols in every piece, only one of them ever resolving.
689
+ api, _ = client_with([
690
+ self._refusal(),
691
+ piece("https://r2.test/a.parquet", "2026-08-21", {
692
+ "symbols": ["NVDA", "NOSUCH"],
693
+ "resolved": {"NVDA": [{"start": "2026-08-20",
694
+ "end": "2026-09-03", "value": "42"}]},
695
+ "not_found": ["NOSUCH"],
696
+ }),
697
+ piece("https://r2.test/b.parquet", "2026-09-04", {
698
+ "symbols": ["NVDA", "NOSUCH"],
699
+ "resolved": {"NVDA": [{"start": "2026-09-03",
700
+ "end": "2026-09-17", "value": "42"}]},
701
+ "not_found": ["NOSUCH"],
702
+ }),
703
+ ])
704
+ monkeypatch.setattr(
705
+ "quantpad_data.v2._read_parquet_url",
706
+ lambda url, columns: pd.DataFrame({"close": [100.0]}),
707
+ )
708
+
709
+ frame = api.timeseries.get_range(
710
+ dataset="XNAS.ITCH", symbols=["NVDA", "NOSUCH"], schema="mbp-10",
711
+ start="2026-08-20", end="2026-09-17", mode="bulk",
712
+ )
713
+ resolution = frame.attrs["quantpad"]["resolution"]
714
+ assert resolution["not_found"] == ["NOSUCH"]
715
+ assert "NOSUCH" not in resolution["resolved"]
716
+
665
717
  def test_an_unsplit_bulk_read_is_labelled_exactly_as_before(
666
718
  self, monkeypatch
667
719
  ):
File without changes
File without changes