quantpad-data 0.7.1__tar.gz → 0.8.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.1 → quantpad_data-0.8.1}/PKG-INFO +8 -5
  2. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/README.md +7 -4
  3. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/pyproject.toml +1 -1
  4. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/__init__.py +6 -2
  5. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/client.py +1 -1
  6. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/memory.py +1 -1
  7. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/v2.py +64 -26
  8. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_packaging.py +38 -0
  9. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_v2.py +76 -3
  10. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/.gitignore +0 -0
  11. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/cache.py +0 -0
  12. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/errors.py +0 -0
  13. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/lean.py +0 -0
  14. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/py.typed +0 -0
  15. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/quantpad_data/rth.py +0 -0
  16. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/conftest.py +0 -0
  17. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_client.py +0 -0
  18. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_lean.py +0 -0
  19. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_message_encoding.py +0 -0
  20. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_refusal_hint.py +0 -0
  21. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_rth.py +0 -0
  22. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_v2_cache.py +0 -0
  23. {quantpad_data-0.7.1 → quantpad_data-0.8.1}/tests/test_v2_gate.py +0 -0
  24. {quantpad_data-0.7.1 → quantpad_data-0.8.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.1
3
+ Version: 0.8.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
@@ -106,9 +106,8 @@ symbol it is. That is convenient until it is wrong, and when it is wrong it
106
106
  fails quietly — asking for a specific futures contract returns an empty frame
107
107
  that looks exactly like a coverage gap.
108
108
 
109
- `qpd.v2.Historical` removes the inference. It takes the same arguments as
110
- `databento.Historical`, so code written against Databento ports over by
111
- swapping the client:
109
+ `qpd.v2.Historical` removes the inference. Every call names its dataset and
110
+ symbology type explicitly, and every response reports the address it used:
112
111
 
113
112
  ```python
114
113
  import quantpad_data as qpd
@@ -243,7 +242,11 @@ cache.clear() # throw it all away
243
242
  (`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
244
243
  `get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
245
244
  `client.options.chain(...)` for enumerating strikes and expiries without
246
- downloading a day's entire `definition` schema.
245
+ downloading a day's entire `definition` schema. A chain is capped at `limit`
246
+ (5,000 by default, 50,000 at most); the frame's `attrs` carry the server's
247
+ `count`, `total` and `truncated`, and a cut chain raises
248
+ `TruncatedChainWarning` — `warnings.simplefilter("error",
249
+ qpd.TruncatedChainWarning)` makes a partial chain a hard failure.
247
250
 
248
251
  There is no cost or unit-price surface. QuantPad data is included in the plan
249
252
  rather than metered per request.
@@ -84,9 +84,8 @@ symbol it is. That is convenient until it is wrong, and when it is wrong it
84
84
  fails quietly — asking for a specific futures contract returns an empty frame
85
85
  that looks exactly like a coverage gap.
86
86
 
87
- `qpd.v2.Historical` removes the inference. It takes the same arguments as
88
- `databento.Historical`, so code written against Databento ports over by
89
- swapping the client:
87
+ `qpd.v2.Historical` removes the inference. Every call names its dataset and
88
+ symbology type explicitly, and every response reports the address it used:
90
89
 
91
90
  ```python
92
91
  import quantpad_data as qpd
@@ -221,7 +220,11 @@ cache.clear() # throw it all away
221
220
  (`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
222
221
  `get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
223
222
  `client.options.chain(...)` for enumerating strikes and expiries without
224
- downloading a day's entire `definition` schema.
223
+ downloading a day's entire `definition` schema. A chain is capped at `limit`
224
+ (5,000 by default, 50,000 at most); the frame's `attrs` carry the server's
225
+ `count`, `total` and `truncated`, and a cut chain raises
226
+ `TruncatedChainWarning` — `warnings.simplefilter("error",
227
+ qpd.TruncatedChainWarning)` makes a partial chain a hard failure.
225
228
 
226
229
  There is no cost or unit-price surface. QuantPad data is included in the plan
227
230
  rather than metered per request.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quantpad-data"
7
- version = "0.7.1"
7
+ version = "0.8.1"
8
8
  description = "Official Python client for the QuantPad market-data API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -198,18 +198,22 @@ def iter_l1_mbp1_df_chunks(symbol, start_ms, end_ms, chunksize=250_000,
198
198
 
199
199
  from . import cache # noqa: E402 (submodule: on-disk cache for settled /v2)
200
200
  from . import lean # noqa: E402 (submodule: LEAN backtesting data staging)
201
- from . import v2 # noqa: E402 (submodule: Databento-shaped explicit API)
201
+ from . import v2 # noqa: E402 (submodule: explicit-address API)
202
202
 
203
203
  # Exported at the root as well as on ``v2``: the sandbox module reaches it
204
204
  # as ``qpd.Historical()``, and one spelling across both surfaces means
205
205
  # notebook code moves between them unchanged.
206
206
  Historical = v2.Historical
207
+ # At the root for the same reason: ``warnings.simplefilter("error",
208
+ # qpd.TruncatedChainWarning)`` is how a script turns a cut chain into a
209
+ # failure, and it should not need to know which submodule warns.
210
+ TruncatedChainWarning = v2.TruncatedChainWarning
207
211
 
208
212
  __all__ = [
209
213
  "Client", "QuantPadClient", "QuantPadDataError", "QuantPadDataNotReady", "AuthenticationError",
210
214
  "PermissionDeniedError", "RateLimitError", "QuotaExceededError",
211
215
  "NotFoundError", "V2NotAvailableError", "ValidationError", "UpstreamError",
212
- "ResponseTooLargeError", "TICK_SCHEMAS",
216
+ "ResponseTooLargeError", "TruncatedChainWarning", "TICK_SCHEMAS",
213
217
  "ROLL_ADJUST_MODES", "get_bars", "get_bars_with_retry", "get_ticks",
214
218
  "get_trades", "get_mbp1", "get_mbp10", "get_trades_rth", "get_mbp1_rth",
215
219
  "get_mbp10_rth", "get_spread_1m_rth", "symbology_resolve", "get_universe",
@@ -84,7 +84,7 @@ V2_NOT_AVAILABLE = (
84
84
  "QuantPad support about /v2 access."
85
85
  )
86
86
 
87
- VERSION = "0.7.1"
87
+ VERSION = "0.8.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",
@@ -15,7 +15,7 @@ before fetching it, which is why it takes both.
15
15
 
16
16
  THE UNITS ARE NOT THE SAME, AND THAT IS THE WHOLE DIFFICULTY
17
17
  The service prices in DBN bytes: record count times record width, which is
18
- Databento's own billing arithmetic. This process pays for the compressed wire
18
+ upstream's own billing arithmetic. This process pays for the compressed wire
19
19
  bytes, an Arrow table decoded from them, and a pandas frame copied out of that
20
20
  table -- partly all three at once. Measured with
21
21
  scripts/measure_client_memory.py against production, peak RSS per priced byte
@@ -1,4 +1,4 @@
1
- """Databento-shaped access to QuantPad's cache.
1
+ """Explicit-address access to QuantPad's cache.
2
2
 
3
3
  The v1 helpers take QuantPad's own arguments and infer the rest: which
4
4
  dataset a symbol lives in, what kind of symbol it is, how far back to
@@ -7,18 +7,7 @@ it is wrong it fails quietly — a specific futures contract returns an
7
7
  empty frame that looks exactly like a coverage gap.
8
8
 
9
9
  This module removes the inference. Every call names its dataset and its
10
- symbology type, exactly as ``databento.Historical`` requires, and every
11
- response reports the address it actually used. Code written against the
12
- Databento client transfers over with the client swapped:
13
-
14
- import databento as db
15
- client = db.Historical(key=...)
16
- df = client.timeseries.get_range(
17
- dataset="GLBX.MDP3", symbols="ESZ6", schema="ohlcv-1m",
18
- start="2026-01-05", end="2026-01-06",
19
- ).to_df()
20
-
21
- becomes
10
+ symbology type, and every response reports the address it actually used:
22
11
 
23
12
  import quantpad_data as qpd
24
13
  client = qpd.v2.Historical()
@@ -27,9 +16,9 @@ becomes
27
16
  start="2026-01-05", end="2026-01-06",
28
17
  )
29
18
 
30
- The differences are deliberate and few: a DataFrame comes back directly
31
- rather than a DBNStore, and there is no cost or billing surface, because
32
- data is included in the QuantPad plan rather than metered per request.
19
+ A DataFrame comes back directly, and there is no cost or billing surface,
20
+ because data is included in the QuantPad plan rather than metered per
21
+ request.
33
22
  """
34
23
 
35
24
  from __future__ import annotations
@@ -38,6 +27,7 @@ import io
38
27
  import json
39
28
  import os
40
29
  import tempfile
30
+ import warnings
41
31
  from datetime import date, timedelta
42
32
  from typing import Any, Iterator, Sequence
43
33
 
@@ -254,7 +244,7 @@ class _Namespace:
254
244
 
255
245
 
256
246
  class Timeseries(_Namespace):
257
- """Mirrors ``databento.Historical.timeseries``."""
247
+ """Ranges of bars, ticks and book depth for an explicit address."""
258
248
 
259
249
  def get_range(
260
250
  self,
@@ -762,12 +752,12 @@ class Timeseries(_Namespace):
762
752
 
763
753
 
764
754
  class Symbology(_Namespace):
765
- """Mirrors ``databento.Historical.symbology``, plus a way in.
755
+ """Symbol resolution, plus a way in.
766
756
 
767
757
  Every other call here demands an explicit ``dataset``, which is the
768
758
  point and also a dead end for someone holding the string "AAPL".
769
- ``search`` is the one endpoint that answers that, and it has no
770
- upstream counterpart because Databento assumes you already know.
759
+ ``search`` is the one endpoint that answers that; the rest of this
760
+ surface assumes you already know.
771
761
  """
772
762
 
773
763
  def search(
@@ -829,7 +819,7 @@ class Symbology(_Namespace):
829
819
 
830
820
 
831
821
  class Metadata(_Namespace):
832
- """Mirrors ``databento.Historical.metadata``.
822
+ """Datasets, schemas, fields, coverage and request sizing.
833
823
 
834
824
  The cost and unit-price endpoints are deliberately absent: QuantPad
835
825
  data is included in the plan rather than metered, so a per-request
@@ -920,13 +910,61 @@ class Metadata(_Namespace):
920
910
  return self._client._request("GET", path, params=clean).json()
921
911
 
922
912
 
913
+ class TruncatedChainWarning(UserWarning):
914
+ """``options.chain`` returned fewer contracts than the filters matched.
915
+
916
+ The service caps a chain at ``limit`` (5,000 by default, 50,000 at
917
+ most) and says so in the body; this is that flag made audible. Raise
918
+ ``limit`` or narrow the filters (``expiration``, ``strike_min``/
919
+ ``strike_max``, ``instrument_class``) for the whole chain.
920
+ """
921
+
922
+
923
+ def _chain_frame(payload: dict) -> pd.DataFrame:
924
+ """The chain as a frame, carrying the server's count beside the rows.
925
+
926
+ The route answers ``count`` (rows in the body), ``total`` (rows the
927
+ filters matched) and ``truncated`` beside ``contracts``. Until this
928
+ the frame was built from ``contracts`` alone and the other three were
929
+ dropped, so a notebook handed exactly 5,000 rows could not tell a cut
930
+ chain from a whole one -- verified on prod, where an unfiltered
931
+ SPY.OPT came back as exactly the default limit against ~12,470
932
+ instruments. The three ride on ``frame.attrs`` (the same place
933
+ ``get_range`` keeps its echo) and a truncation is also warned, since
934
+ ``attrs`` is invisible to anyone not already looking for it.
935
+
936
+ Older servers send no ``total``; it is then unknown rather than
937
+ guessed, and ``truncated`` is read as the server sent it.
938
+ """
939
+ contracts = payload.get("contracts", [])
940
+ frame = pd.DataFrame(contracts)
941
+ truncated = bool(payload.get("truncated", False))
942
+ frame.attrs["count"] = payload.get("count", len(contracts))
943
+ frame.attrs["total"] = payload.get("total")
944
+ frame.attrs["truncated"] = truncated
945
+ if truncated:
946
+ total = frame.attrs["total"]
947
+ matched = f"{total:,} matched" if isinstance(total, int) else "more matched"
948
+ warnings.warn(
949
+ f"options.chain returned {len(contracts):,} contracts but "
950
+ f"{matched}: the chain was cut at the service's limit. Raise "
951
+ "limit= (up to 50,000) or narrow the filters to see the rest.",
952
+ TruncatedChainWarning,
953
+ stacklevel=3,
954
+ )
955
+ return frame
956
+
957
+
923
958
  class Options(_Namespace):
924
959
  """Options-chain enumeration.
925
960
 
926
- Not part of Databento's client surface — upstream expects you to pull
927
- the whole ``definition`` schema and filter it yourself. This does
961
+ Not part of the usual historical-client surface, which expects you to
962
+ pull the whole ``definition`` schema and filter it yourself. This does
928
963
  that server-side, because "download every instrument definition for
929
964
  the day" is a poor answer to "what strikes exist".
965
+
966
+ The frame's ``attrs`` carry ``count``, ``total`` and ``truncated``;
967
+ a truncated chain also raises :class:`TruncatedChainWarning`.
930
968
  """
931
969
 
932
970
  def chain(
@@ -955,11 +993,11 @@ class Options(_Namespace):
955
993
  payload = self._client._request(
956
994
  "GET", "/v2/options/chain", params=clean
957
995
  ).json()
958
- return pd.DataFrame(payload.get("contracts", []))
996
+ return _chain_frame(payload)
959
997
 
960
998
 
961
999
  class Batch(_Namespace):
962
- """Mirrors ``databento.Historical.batch``, scoped to your own jobs.
1000
+ """Batch extracts, scoped to your own jobs.
963
1001
 
964
1002
  Useful when a range is too large to stream in one sitting: submit,
965
1003
  walk away, then hydrate the result into the cache so ordinary
@@ -1010,7 +1048,7 @@ class Batch(_Namespace):
1010
1048
 
1011
1049
 
1012
1050
  class Historical:
1013
- """A ``databento.Historical``-shaped view of QuantPad's cache.
1051
+ """An explicit-address view of QuantPad's cache.
1014
1052
 
1015
1053
  Takes the same credentials as the v1 ``Client`` — an API key, or the
1016
1054
  injected token when running inside a QuantPad sandbox.
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  import ast
23
23
  import os
24
+ import re
24
25
  import subprocess
25
26
  import sys
26
27
  import tarfile
@@ -53,6 +54,7 @@ def built(tmp_path_factory):
53
54
  # ./dist would make it skip in CI, where nothing has built there yet --
54
55
  # a test that skips exactly where it is needed.
55
56
  "wheel_path": wheel,
57
+ "sdist_path": sdist,
56
58
  }
57
59
 
58
60
 
@@ -182,3 +184,39 @@ class TestAFreshInstallExposesTheApi:
182
184
  f"a fresh install cannot answer for its own API:\n"
183
185
  f"{done.stdout}\n{done.stderr[-1500:]}"
184
186
  )
187
+
188
+
189
+ class TestNothingShippedNamesTheDataVendor:
190
+ """QuantPad presents its market data as its own, and a published package is
191
+ as public as a response: PyPI serves every file in both artifacts, the
192
+ README is the project page, and ``help()`` in a user's sandbox prints the
193
+ docstrings. So nothing shipped may name the vendor -- read out of the
194
+ artifacts, for the same reason as everything else in this file."""
195
+
196
+ VENDOR = re.compile(r"data[\s_-]?bento", re.IGNORECASE)
197
+
198
+ @staticmethod
199
+ def _texts(built, kind):
200
+ if kind == "wheel":
201
+ with zipfile.ZipFile(built["wheel_path"]) as archive:
202
+ for name in archive.namelist():
203
+ yield name, archive.read(name)
204
+ else:
205
+ with tarfile.open(built["sdist_path"]) as archive:
206
+ for member in archive.getmembers():
207
+ if member.isfile():
208
+ yield member.name, archive.extractfile(member).read()
209
+
210
+ @pytest.mark.parametrize("kind", ["wheel", "sdist"])
211
+ def test_no_shipped_file_names_the_vendor(self, built, kind):
212
+ offenders = [
213
+ name
214
+ for name, data in self._texts(built, kind)
215
+ if self.VENDOR.search(data.decode("utf-8", errors="replace"))
216
+ ]
217
+ assert not offenders, f"the {kind} names the data vendor in {offenders}"
218
+
219
+ def test_the_readme_is_in_what_was_checked(self, built):
220
+ """Guards the test above: the wheel's METADATA carries the README."""
221
+ names = [name for name, _ in self._texts(built, "wheel")]
222
+ assert any(name.endswith("METADATA") for name in names)
@@ -1,4 +1,4 @@
1
- """The Databento-shaped surface.
1
+ """The explicit-address surface.
2
2
 
3
3
  Two things are being checked. First that the wire calls carry exactly
4
4
  what the caller said and nothing the SDK invented — the whole reason
@@ -95,7 +95,7 @@ class TestNothingIsInferred:
95
95
  assert kwargs["json"]["symbols"] == ["ESZ6"]
96
96
 
97
97
  def test_a_single_symbol_string_is_sent_as_a_list(self):
98
- """Databento accepts either; normalising here keeps the server
98
+ """Upstream accepts either; normalising here keeps the server
99
99
  contract one shape instead of two."""
100
100
  api, session = client_with([response(200, arrow_bytes(bars_table()))])
101
101
  api.timeseries.get_range(
@@ -104,7 +104,7 @@ class TestNothingIsInferred:
104
104
  )
105
105
  assert session.calls[0][2]["json"]["symbols"] == ["ESZ6"]
106
106
 
107
- def test_the_default_stype_matches_databento_not_our_own_convention(self):
107
+ def test_the_default_stype_matches_upstream_not_our_own_convention(self):
108
108
  api, session = client_with([response(200, arrow_bytes(bars_table()))])
109
109
  api.timeseries.get_range(
110
110
  dataset="GLBX.MDP3", symbols="ESZ6", schema="ohlcv-1m",
@@ -2117,6 +2117,79 @@ class TestTheOtherNamespaces:
2117
2117
  assert "strike_min" not in params
2118
2118
  assert "instrument_class" not in params
2119
2119
 
2120
+ # chaintruncate: the route says when it cut the chain, and the frame
2121
+ # used to drop that. Verified on prod: unfiltered SPY.OPT returned
2122
+ # exactly 5,000 rows (the default limit) against ~12,470 instruments,
2123
+ # with no way to tell from the frame.
2124
+
2125
+ def test_a_cut_chain_warns_and_carries_the_servers_count(self):
2126
+ import warnings
2127
+
2128
+ api, _ = client_with([json_response({
2129
+ "count": 2, "total": 12470, "truncated": True,
2130
+ "contracts": [
2131
+ {"raw_symbol": "SPY 260116C00500000"},
2132
+ {"raw_symbol": "SPY 260116P00500000"},
2133
+ ],
2134
+ })])
2135
+ with warnings.catch_warnings(record=True) as caught:
2136
+ warnings.simplefilter("always")
2137
+ chain = api.options.chain(
2138
+ dataset="OPRA.PILLAR", underlier="SPY.OPT", date="2026-01-05",
2139
+ )
2140
+ assert len(chain) == 2
2141
+ assert chain.attrs["truncated"] is True
2142
+ assert chain.attrs["count"] == 2
2143
+ assert chain.attrs["total"] == 12470
2144
+ (warning,) = [
2145
+ w for w in caught if issubclass(w.category, quantpad_data.TruncatedChainWarning)
2146
+ ]
2147
+ text = str(warning.message)
2148
+ assert "2 contracts" in text and "12,470 matched" in text
2149
+ assert "limit=" in text # the way out is named, not just the fact
2150
+
2151
+ def test_a_whole_chain_is_silent_and_says_so(self):
2152
+ import warnings
2153
+
2154
+ api, _ = client_with([json_response({
2155
+ "count": 1, "total": 1, "truncated": False,
2156
+ "contracts": [{"raw_symbol": "ESM6 C4800"}],
2157
+ })])
2158
+ with warnings.catch_warnings():
2159
+ warnings.simplefilter("error") # any warning here is a failure
2160
+ chain = api.options.chain(
2161
+ dataset="GLBX.MDP3", underlier="ES.OPT", date="2026-01-05",
2162
+ )
2163
+ assert chain.attrs["truncated"] is False
2164
+ assert chain.attrs["total"] == 1
2165
+
2166
+ def test_an_older_server_without_total_still_reports_the_cut(self):
2167
+ """The deployed route sent ``truncated`` before it sent ``total``;
2168
+ a frame from it must still say it was cut, with the total unknown
2169
+ rather than invented."""
2170
+ import warnings
2171
+
2172
+ api, _ = client_with([json_response({
2173
+ "count": 1, "truncated": True, "contracts": [{"raw_symbol": "A"}],
2174
+ })])
2175
+ with warnings.catch_warnings(record=True) as caught:
2176
+ warnings.simplefilter("always")
2177
+ chain = api.options.chain(
2178
+ dataset="GLBX.MDP3", underlier="ES.OPT", date="2026-01-05",
2179
+ )
2180
+ assert chain.attrs["truncated"] is True
2181
+ assert chain.attrs["total"] is None
2182
+ assert any(
2183
+ issubclass(w.category, quantpad_data.TruncatedChainWarning) for w in caught
2184
+ )
2185
+
2186
+ def test_the_truncation_warning_is_exported_at_the_root(self):
2187
+ """``warnings.simplefilter("error", quantpad_data.TruncatedChainWarning)`` is
2188
+ how a script refuses to run on a partial chain; it needs the name
2189
+ without knowing which submodule warns."""
2190
+ assert quantpad_data.TruncatedChainWarning is quantpad_data.v2.TruncatedChainWarning
2191
+ assert issubclass(quantpad_data.TruncatedChainWarning, UserWarning)
2192
+
2120
2193
  def test_batch_submit_and_hydrate_are_reachable(self):
2121
2194
  api, session = client_with([
2122
2195
  json_response({"job_id": "j1"}),
File without changes