SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

tests: flux/screener avec de vraies clés (le middleware de quotas valide les clés avant le handler)

Simon-Pierre Boucher committed 20 days ago (Sep 5, 2026) parent 7a7c407

2 changed files +42 −32

modified tests/test_fundamentals_api.py +10 −10
@@ -178,28 +178,28 @@ def test_ratios_daily_point_in_time(client):
178 178 assert r.status_code == 200 and len(pd.read_parquet(io.BytesIO(r.content))) == int(r.headers["X-Row-Count"])
179 179
180 180
181 −def test_screener(client):
182 − r = client.get("/v1/fundamentals/screener?filters=market_cap>1b&sort=market_cap:desc")
181 +def test_screener(client_hu):
182 + r = client_hu.get("/v1/fundamentals/screener?filters=market_cap>1b&sort=market_cap:desc")
183 183 assert r.status_code == 200
184 184 body = r.json()
185 185 tickers = [x["ticker"] for x in body["data"]]
186 186 assert tickers[:2] == ["AAPL", "MSFT"] and body["meta"]["total"] >= 2
187 − r = client.get("/v1/fundamentals/screener?filters=ticker=SHAK&columns=ticker,revenue,gross_margin,net_margin")
187 + r = client_hu.get("/v1/fundamentals/screener?filters=ticker=SHAK&columns=ticker,revenue,gross_margin,net_margin")
188 188 row = r.json()["data"][0]
189 189 assert row["ticker"] == "SHAK" and row["gross_margin"] is None and row["net_margin"] is not None
190 − r = client.get("/v1/fundamentals/screener?filters=revenue=1..2,pe>0")
190 + r = client_hu.get("/v1/fundamentals/screener?filters=revenue=1..2,pe>0")
191 191 assert r.status_code == 200 and r.json()["meta"]["count"] == 0
192 − r = client.get("/v1/fundamentals/screener?filters=roe>10000%25")
192 + r = client_hu.get("/v1/fundamentals/screener?filters=roe>10000%25")
193 193 assert r.status_code == 200 and r.json()["meta"]["count"] == 0
194 − r = client.get("/v1/fundamentals/screener?filters=pe<<15")
194 + r = client_hu.get("/v1/fundamentals/screener?filters=pe<<15")
195 195 assert r.status_code == 400 and r.json()["error"]["code"] == "INVALID_FILTER"
196 − r = client.get("/v1/fundamentals/screener?filters=unknown>1")
196 + r = client_hu.get("/v1/fundamentals/screener?filters=unknown>1")
197 197 assert r.status_code == 400 and "numeric_fields" in r.json()["error"]["details"]
198 − r = client.get("/v1/fundamentals/screener?limit=1&format=csv")
198 + r = client_hu.get("/v1/fundamentals/screener?limit=1&format=csv")
199 199 assert r.status_code == 200 and r.headers["X-Row-Count"] == "1"
200 − r = client.get("/v1/fundamentals/screener?limit=1")
200 + r = client_hu.get("/v1/fundamentals/screener?limit=1")
201 201 nxt = r.json()["meta"]["next_cursor"]
202 − assert nxt and client.get(f"/v1/fundamentals/screener?limit=1&cursor={nxt}").json()["data"][0]["ticker"] != r.json()["data"][0]["ticker"]
202 + assert nxt and client_hu.get(f"/v1/fundamentals/screener?limit=1&cursor={nxt}").json()["data"][0]["ticker"] != r.json()["data"][0]["ticker"]
203 203
204 204
205 205 def test_frames(client):
modified tests/test_stream.py +32 −22
@@ -4,8 +4,14 @@ from __future__ import annotations
4 4 import pytest
5 5 from starlette.websockets import WebSocketDisconnect
6 6
7 −KEY = "hfmd_live_" + "a" * 32
8 −KEY2 = "hfmd_live_" + "b" * 32
7 +
8 +
9 +@pytest.fixture
10 +def keys(make_user):
11 + """Two real high_usage API keys (the rate-limit middleware validates keys before the stream handler)."""
12 + _, k1, _, _ = make_user(tier="high_usage")
13 + _, k2, _, _ = make_user(tier="high_usage")
14 + return k1, k2
9 15
10 16
11 17 @pytest.fixture(autouse=True)
@@ -25,16 +31,20 @@ def test_keyless_is_refused_with_json_reason(client):
25 31
26 32
27 33 def test_bad_key_is_refused(client):
28 − with client.websocket_connect("/v1/stream", headers={"Authorization": "Bearer not-a-key"}) as ws:
29 − assert ws.receive_json()["code"] == "AUTH_REQUIRED"
34 + try:
35 + with client.websocket_connect("/v1/stream", headers={"Authorization": "Bearer not-a-key"}) as ws:
36 + msg = ws.receive_json()
37 + assert msg["code"] in ("AUTH_REQUIRED", "INVALID_API_KEY")
38 + except WebSocketDisconnect as e: # refused by the rate-limit middleware before the handler
39 + assert e.code in (4401, 4001, 1008)
30 40
31 41
32 −def test_subscribe_receive_and_accounting(client, fundamentals_data):
42 +def test_subscribe_receive_and_accounting(keys, client, fundamentals_data):
33 43 from stream import accounting, broker
34 44 from stream.auth import authenticate
35 − principal = authenticate(KEY)
45 + principal = authenticate(keys[0])
36 46 before = accounting.rows_charged(principal)
37 − with client.websocket_connect(f"/v1/stream?api_key={KEY}") as ws:
47 + with client.websocket_connect(f"/v1/stream?api_key={keys[0]}") as ws:
38 48 hello = ws.receive_json()
39 49 assert hello["type"] == "hello" and hello["heartbeat_seconds"] == 20
40 50 ws.send_json({"action": "subscribe", "channel": "filings", "tickers": ["AAPL"], "forms": ["10-Q", "10-K"]})
@@ -53,11 +63,11 @@ def test_subscribe_receive_and_accounting(client, fundamentals_data):
53 63 assert accounting.rows_charged(principal) == before + 1
54 64
55 65
56 −def test_real_filing_event_from_ingest(client, fundamentals_data):
66 +def test_real_filing_event_from_ingest(keys, client, fundamentals_data):
57 67 from datetime import date
58 68
59 69 from fundamentals import ingest
60 − with client.websocket_connect(f"/v1/stream?api_key={KEY}") as ws:
70 + with client.websocket_connect(f"/v1/stream?api_key={keys[0]}") as ws:
61 71 ws.receive_json()
62 72 ws.send_json({"action": "subscribe", "channel": "filings", "tickers": "all"})
63 73 ws.receive_json()
@@ -73,18 +83,18 @@ def test_real_filing_event_from_ingest(client, fundamentals_data):
73 83 assert s["yoy"]["revenue"] == pytest.approx(90_753 / 94_836 - 1, rel=1e-4)
74 84
75 85
76 −def test_resume_token_replays_buffer(client):
86 +def test_resume_token_replays_buffer(keys, client):
77 87 from stream import broker
78 88 s1 = broker.publish({"type": "filing", "ticker": "AAPL", "form": "10-K", "accn": "r1"})
79 89 s2 = broker.publish({"type": "filing", "ticker": "AAPL", "form": "10-K", "accn": "r2"})
80 90 s3 = broker.publish({"type": "filing", "ticker": "AAPL", "form": "10-K", "accn": "r3"})
81 − with client.websocket_connect(f"/v1/stream?api_key={KEY}") as ws:
91 + with client.websocket_connect(f"/v1/stream?api_key={keys[0]}") as ws:
82 92 assert ws.receive_json()["seq"] == s3
83 93 ws.send_json({"action": "subscribe", "channel": "filings", "tickers": ["AAPL"], "resume_token": s1})
84 94 assert ws.receive_json()["resume_from"] == s1
85 95 assert [ws.receive_json()["accn"] for _ in range(2)] == ["r2", "r3"]
86 96 assert s2 < s3
87 − with client.websocket_connect(f"/v1/stream?api_key={KEY}") as ws: # without resume: only new events
97 + with client.websocket_connect(f"/v1/stream?api_key={keys[0]}") as ws: # without resume: only new events
88 98 ws.receive_json()
89 99 ws.send_json({"action": "subscribe"})
90 100 ws.receive_json()
@@ -92,8 +102,8 @@ def test_resume_token_replays_buffer(client):
92 102 assert ws.receive_json()["accn"] == "r4"
93 103
94 104
95 −def test_protocol_errors(client):
96 − with client.websocket_connect(f"/v1/stream?api_key={KEY}") as ws:
105 +def test_protocol_errors(keys, client):
106 + with client.websocket_connect(f"/v1/stream?api_key={keys[0]}") as ws:
97 107 ws.receive_json()
98 108 ws.send_text("not json")
99 109 assert ws.receive_json()["code"] == "VALIDATION_ERROR"
@@ -107,31 +117,31 @@ def test_protocol_errors(client):
107 117 assert ws.receive_json()["code"] == "VALIDATION_ERROR"
108 118
109 119
110 −def test_heartbeat(client, monkeypatch):
120 +def test_heartbeat(keys, client, monkeypatch):
111 121 from stream import routes
112 122 monkeypatch.setattr(routes, "HEARTBEAT_SECONDS", 0.3)
113 − with client.websocket_connect(f"/v1/stream?api_key={KEY2}") as ws:
123 + with client.websocket_connect(f"/v1/stream?api_key={keys[1]}") as ws:
114 124 ws.receive_json()
115 125 beat = ws.receive_json()
116 126 assert beat["type"] == "heartbeat" and "ts" in beat and "seq" in beat
117 127
118 128
119 −def test_connection_limit_per_key(client):
129 +def test_connection_limit_per_key(keys, client):
120 130 from contextlib import ExitStack
121 131
122 132 from stream import routes
123 133 with ExitStack() as stack:
124 134 for _ in range(routes.MAX_CONNECTIONS_PER_KEY):
125 − ws = stack.enter_context(client.websocket_connect(f"/v1/stream?api_key={KEY}"))
135 + ws = stack.enter_context(client.websocket_connect(f"/v1/stream?api_key={keys[0]}"))
126 136 assert ws.receive_json()["type"] == "hello"
127 − extra = stack.enter_context(client.websocket_connect(f"/v1/stream?api_key={KEY}"))
137 + extra = stack.enter_context(client.websocket_connect(f"/v1/stream?api_key={keys[0]}"))
128 138 msg = extra.receive_json()
129 139 assert msg["type"] == "error" and msg["code"] == "STREAM_CONNECTION_LIMIT" and msg["limit"] == 5
130 140 with pytest.raises(WebSocketDisconnect) as e:
131 141 extra.receive_json()
132 142 assert e.value.code == 4029
133 143 # slots are released on disconnect
134 − with client.websocket_connect(f"/v1/stream?api_key={KEY}") as ws:
144 + with client.websocket_connect(f"/v1/stream?api_key={keys[0]}") as ws:
135 145 assert ws.receive_json()["type"] == "hello"
136 146
137 147
@@ -146,8 +156,8 @@ def test_accounting_hook_interface(app):
146 156 accounting.set_charger(None)
147 157
148 158
149 −def test_stream_info(client):
150 − r = client.get("/v1/stream/info")
159 +def test_stream_info(client_hu):
160 + r = client_hu.get("/v1/stream/info")
151 161 assert r.status_code == 200
152 162 d = r.json()["data"]
153 163 assert d["channels"] == ["filings"] and d["close_codes"]["4001"] == "AUTH_REQUIRED" and d["url"].startswith("wss://")
154 164