fix: fetch dataset scope options from WorldQuant platform
This commit is contained in:
@@ -109,6 +109,8 @@ def create_test_app():
|
||||
},
|
||||
headers={"Set-Cookie": "mock=only; Path=/"},
|
||||
)
|
||||
if request.method == "OPTIONS":
|
||||
return catalog_response(request) or httpx.Response(404)
|
||||
if path.startswith("/simulations") or (
|
||||
path.startswith("/alphas/") and path.rsplit("/", 1)[-1] in simulations.alphas
|
||||
):
|
||||
|
||||
@@ -21,6 +21,8 @@ def field_records(dataset="TEST_FIN", count=123):
|
||||
|
||||
def catalog_response(request, fields=None):
|
||||
path, params = request.url.path, request.url.params
|
||||
if request.method == "OPTIONS" and path == "/simulations":
|
||||
return httpx.Response(200, json=platform_response())
|
||||
if path not in ("/data-sets", "/data-fields"):
|
||||
return None
|
||||
assert request.method == "GET"
|
||||
@@ -53,3 +55,18 @@ def catalog_response(request, fields=None):
|
||||
rows = rows[:50] + [rows[49]] + rows[50:]
|
||||
offset, limit = int(params.get("offset", 0)), int(params.get("limit", 50))
|
||||
return httpx.Response(200, json={"results": rows[offset : offset + limit]})
|
||||
|
||||
|
||||
def platform_response():
|
||||
"""Synthetic upstream options include a previously unsupported market and delay."""
|
||||
def values(items):
|
||||
return [{"value": item} for item in items]
|
||||
regions = {"USA": ["TOP3000", "TOP1000"], "CHN": ["TOP2000U"], "IND": ["TOP500"]}
|
||||
children = {"instrumentType": {"choices": values(["EQUITY"])},
|
||||
"region": {"choices": {"instrumentType": {"EQUITY": values(regions)}}}}
|
||||
for key in ("universe", "delay"):
|
||||
children[key] = {"choices": {"instrumentType": {"EQUITY": {"region": {
|
||||
region: values(universes if key == "universe" else [0, 1] if region == "USA" else [1])
|
||||
for region, universes in regions.items()
|
||||
}}}}}
|
||||
return {"actions": {"POST": {"settings": {"children": children}}}}
|
||||
|
||||
@@ -26,6 +26,8 @@ async def catalog(logged_in, app):
|
||||
401, headers={"WWW-Authenticate": "persona", "Location": "/authentication/persona/test"}
|
||||
)
|
||||
return httpx.Response(201, json={"token": {"expiry": 14400}})
|
||||
if request.method == "OPTIONS":
|
||||
return catalog_response(request)
|
||||
assert request.method == "GET"
|
||||
if request.url.path == "/users/self":
|
||||
return httpx.Response(200, json={"id": "TEST_USER"})
|
||||
@@ -209,7 +211,7 @@ async def test_scope_ownership_empty_and_unknown_fields_are_rejected(catalog):
|
||||
).status_code == 422
|
||||
assert (await prepare(client, version, dataset_id="TEST_NEWS")).status_code == 409
|
||||
assert (await prepare(client, version, scope={**SCOPE, "delay": 0})).status_code == 404
|
||||
assert (await prepare(client, version, scope={**SCOPE, "region": "CHN"})).status_code == 422
|
||||
assert (await prepare(client, version, scope={**SCOPE, "region": "CHN"})).status_code == 404
|
||||
subset = await prepare(client, version, selection="explicit", excluded_ids=["TEST_FIN_110"])
|
||||
assert subset.status_code == 201 and len(subset.json()["field_ids"]) == 122
|
||||
assert "TEST_FIN_110" not in subset.json()["field_ids"]
|
||||
@@ -241,9 +243,11 @@ async def test_retry_after_auth_wait_disconnect_and_collection_manifest(catalog)
|
||||
assert job["status"] == "completed" and delays == [2]
|
||||
manifest = await search(client, "/datasets/TEST_FIN/collection")
|
||||
assert manifest["collection_version"] == job["id"] and len(manifest["field_ids"]) == 123
|
||||
queued = await client.post(BASE + "/sync-jobs", json={"scope": SCOPE, "dataset_id": "TEST_FIN"})
|
||||
state["persona"] = True
|
||||
runner.client.authenticated = False
|
||||
waiting = await sync(catalog, "TEST_FIN")
|
||||
await runner.execute(queued.json()["id"])
|
||||
waiting = (await client.get("/api/v1/sync-jobs/" + queued.json()["id"])).json()
|
||||
assert waiting["status"] == "waiting_auth"
|
||||
assert (await search(client, "/datasets/TEST_FIN/collection")) == manifest
|
||||
await runner.disconnect()
|
||||
@@ -255,3 +259,19 @@ async def test_retry_after_auth_wait_disconnect_and_collection_manifest(catalog)
|
||||
await runner.execute(connect["id"])
|
||||
await runner.execute(waiting["id"])
|
||||
assert (await client.get("/api/v1/sync-jobs/" + waiting["id"])).json()["status"] == "completed"
|
||||
|
||||
|
||||
async def test_dynamic_platform_scopes_and_validation(catalog):
|
||||
client, runner, _ = catalog
|
||||
options = (await client.get(BASE + "/scopes")).json()
|
||||
assert "IND" in options["regions_by_type"]["EQUITY"]
|
||||
assert (await sync(catalog, scope={**SCOPE, "region": "IND", "universe": "TOP500"}))["status"] == "completed"
|
||||
invalid = await client.post(BASE + "/sync-jobs", json={"scope": {**SCOPE, "region": "IND"}})
|
||||
assert invalid.status_code == 422
|
||||
from app.ai.tools import EmptyArgs, read_tool
|
||||
ai = await read_tool(None, "get_catalog_scopes", EmptyArgs(), runner.client)
|
||||
assert ai["instrument_options"] == options["instrument_options"]
|
||||
assert ai["_meta"]["source"] == "worldquant_platform"
|
||||
runner.client.disconnect()
|
||||
assert (await client.get(BASE + "/scopes")).status_code == 409
|
||||
assert (await search(client))["total"] == 0
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Exercise OPTIONS through the real authenticated adapter, without live account traffic."""
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.worldquant import WqClient, WqError
|
||||
from tests.catalog_fake import platform_response
|
||||
|
||||
|
||||
async def test_options_refreshes_auth_and_keeps_new_choices(settings):
|
||||
calls = []
|
||||
auth = 0
|
||||
|
||||
def handler(request):
|
||||
nonlocal auth
|
||||
calls.append((request.method, request.url.path))
|
||||
if request.url.path == "/authentication":
|
||||
auth += 1
|
||||
return httpx.Response(201, json={})
|
||||
assert request.method == "OPTIONS" and request.url.path == "/simulations"
|
||||
if auth == 1:
|
||||
return httpx.Response(401)
|
||||
data = platform_response()
|
||||
children = data["actions"]["POST"]["settings"]["children"]
|
||||
children["delay"]["choices"]["instrumentType"]["EQUITY"]["region"]["IND"] = [{"value": 2}]
|
||||
return httpx.Response(200, json=data)
|
||||
|
||||
client = WqClient(settings, transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
await client.authenticate("test@example.com", "test-only")
|
||||
data = await client.get_platform_setting_options()
|
||||
assert auth == 2
|
||||
assert {"instrument_type": "EQUITY", "region": "IND", "delay": 2,
|
||||
"universes": ["TOP500"]} in data["instrument_options"]
|
||||
assert all(method == "OPTIONS" or path == "/authentication" for method, path in calls)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("body", [{}, {"actions": None}, {"actions": {"POST": {"settings": {"children": []}}}}])
|
||||
async def test_malformed_options_fail_without_static_fallback(settings, body):
|
||||
client = WqClient(settings, transport=httpx.MockTransport(lambda _: httpx.Response(200, json=body)))
|
||||
client.credentials, client.authenticated = ("test", "test"), True
|
||||
try:
|
||||
with pytest.raises(WqError) as error:
|
||||
await client.get_platform_setting_options()
|
||||
assert error.value.code == "invalid_response"
|
||||
finally:
|
||||
await client.close()
|
||||
Reference in New Issue
Block a user