lumibot 4.2.7__py3-none-any.whl → 4.2.9__py3-none-any.whl
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.
Potentially problematic release.
This version of lumibot might be problematic. Click here for more details.
- lumibot/tools/thetadata_helper.py +101 -38
- {lumibot-4.2.7.dist-info → lumibot-4.2.9.dist-info}/METADATA +1 -1
- {lumibot-4.2.7.dist-info → lumibot-4.2.9.dist-info}/RECORD +7 -7
- tests/test_thetadata_helper.py +76 -3
- {lumibot-4.2.7.dist-info → lumibot-4.2.9.dist-info}/WHEEL +0 -0
- {lumibot-4.2.7.dist-info → lumibot-4.2.9.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.7.dist-info → lumibot-4.2.9.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,7 @@ CONNECTION_RETRY_SLEEP = 1.0
|
|
|
26
26
|
CONNECTION_MAX_RETRIES = 60
|
|
27
27
|
BOOT_GRACE_PERIOD = 5.0
|
|
28
28
|
MAX_RESTART_ATTEMPTS = 3
|
|
29
|
+
MAX_TERMINAL_RESTART_CYCLES = 3
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def _resolve_asset_folder(asset_obj: Asset) -> str:
|
|
@@ -43,6 +44,12 @@ THETA_DATA_PROCESS = None
|
|
|
43
44
|
THETA_DATA_PID = None
|
|
44
45
|
THETA_DATA_LOG_HANDLE = None
|
|
45
46
|
|
|
47
|
+
|
|
48
|
+
class ThetaDataConnectionError(RuntimeError):
|
|
49
|
+
"""Raised when ThetaTerminal cannot reconnect to Theta Data after multiple restarts."""
|
|
50
|
+
|
|
51
|
+
pass
|
|
52
|
+
|
|
46
53
|
def reset_connection_diagnostics():
|
|
47
54
|
"""Reset ThetaData connection counters (useful for tests)."""
|
|
48
55
|
CONNECTION_DIAGNOSTICS.update({
|
|
@@ -50,6 +57,7 @@ def reset_connection_diagnostics():
|
|
|
50
57
|
"start_terminal_calls": 0,
|
|
51
58
|
"network_requests": 0,
|
|
52
59
|
"placeholder_writes": 0,
|
|
60
|
+
"terminal_restarts": 0,
|
|
53
61
|
})
|
|
54
62
|
|
|
55
63
|
|
|
@@ -198,6 +206,7 @@ CONNECTION_DIAGNOSTICS = {
|
|
|
198
206
|
"start_terminal_calls": 0,
|
|
199
207
|
"network_requests": 0,
|
|
200
208
|
"placeholder_writes": 0,
|
|
209
|
+
"terminal_restarts": 0,
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
|
|
@@ -1395,7 +1404,6 @@ def check_connection(username: str, password: str, wait_for_connection: bool = F
|
|
|
1395
1404
|
|
|
1396
1405
|
max_retries = CONNECTION_MAX_RETRIES
|
|
1397
1406
|
sleep_interval = CONNECTION_RETRY_SLEEP
|
|
1398
|
-
restart_attempts = 0
|
|
1399
1407
|
client = None
|
|
1400
1408
|
|
|
1401
1409
|
def probe_status() -> Optional[str]:
|
|
@@ -1435,47 +1443,72 @@ def check_connection(username: str, password: str, wait_for_connection: bool = F
|
|
|
1435
1443
|
logger.debug("ThetaTerminal running but not yet CONNECTED; waiting for status.")
|
|
1436
1444
|
return check_connection(username=username, password=password, wait_for_connection=True)
|
|
1437
1445
|
|
|
1438
|
-
|
|
1439
|
-
connected = False
|
|
1440
|
-
|
|
1441
|
-
while counter < max_retries:
|
|
1442
|
-
status_text = probe_status()
|
|
1443
|
-
if status_text == "CONNECTED":
|
|
1444
|
-
if counter:
|
|
1445
|
-
logger.info("ThetaTerminal connected after %s attempt(s).", counter + 1)
|
|
1446
|
-
connected = True
|
|
1447
|
-
break
|
|
1448
|
-
elif status_text == "DISCONNECTED":
|
|
1449
|
-
logger.debug("ThetaTerminal reports DISCONNECTED; will retry.")
|
|
1450
|
-
elif status_text is not None:
|
|
1451
|
-
logger.debug(f"ThetaTerminal returned unexpected status: {status_text}")
|
|
1452
|
-
|
|
1453
|
-
if not is_process_alive():
|
|
1454
|
-
if restart_attempts >= MAX_RESTART_ATTEMPTS:
|
|
1455
|
-
logger.error("ThetaTerminal not running after %s restart attempts.", restart_attempts)
|
|
1456
|
-
break
|
|
1457
|
-
restart_attempts += 1
|
|
1458
|
-
logger.warning("ThetaTerminal process is not running (restart #%s).", restart_attempts)
|
|
1459
|
-
client = start_theta_data_client(username=username, password=password)
|
|
1460
|
-
time.sleep(max(BOOT_GRACE_PERIOD, sleep_interval))
|
|
1461
|
-
counter = 0
|
|
1462
|
-
continue
|
|
1446
|
+
total_restart_cycles = 0
|
|
1463
1447
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1448
|
+
while True:
|
|
1449
|
+
counter = 0
|
|
1450
|
+
restart_attempts = 0
|
|
1451
|
+
|
|
1452
|
+
while counter < max_retries:
|
|
1453
|
+
status_text = probe_status()
|
|
1454
|
+
if status_text == "CONNECTED":
|
|
1455
|
+
if counter or total_restart_cycles:
|
|
1456
|
+
logger.info(
|
|
1457
|
+
"ThetaTerminal connected after %s attempt(s) (restart cycles=%s).",
|
|
1458
|
+
counter + 1,
|
|
1459
|
+
total_restart_cycles,
|
|
1460
|
+
)
|
|
1461
|
+
return client, True
|
|
1462
|
+
elif status_text == "DISCONNECTED":
|
|
1463
|
+
logger.debug("ThetaTerminal reports DISCONNECTED; will retry.")
|
|
1464
|
+
elif status_text is not None:
|
|
1465
|
+
logger.debug(f"ThetaTerminal returned unexpected status: {status_text}")
|
|
1466
|
+
|
|
1467
|
+
if not is_process_alive():
|
|
1468
|
+
if restart_attempts >= MAX_RESTART_ATTEMPTS:
|
|
1469
|
+
logger.error("ThetaTerminal not running after %s restart attempts.", restart_attempts)
|
|
1470
|
+
break
|
|
1471
|
+
restart_attempts += 1
|
|
1472
|
+
logger.warning("ThetaTerminal process is not running (restart #%s).", restart_attempts)
|
|
1473
|
+
client = start_theta_data_client(username=username, password=password)
|
|
1474
|
+
CONNECTION_DIAGNOSTICS["terminal_restarts"] = CONNECTION_DIAGNOSTICS.get("terminal_restarts", 0) + 1
|
|
1475
|
+
time.sleep(max(BOOT_GRACE_PERIOD, sleep_interval))
|
|
1476
|
+
counter = 0
|
|
1477
|
+
continue
|
|
1468
1478
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1479
|
+
counter += 1
|
|
1480
|
+
if counter % 10 == 0:
|
|
1481
|
+
logger.info("Waiting for ThetaTerminal connection (attempt %s/%s).", counter, max_retries)
|
|
1482
|
+
time.sleep(sleep_interval)
|
|
1483
|
+
|
|
1484
|
+
total_restart_cycles += 1
|
|
1485
|
+
if total_restart_cycles > MAX_TERMINAL_RESTART_CYCLES:
|
|
1486
|
+
logger.error(
|
|
1487
|
+
"Unable to connect to Theta Data after %s restart cycle(s) (%s attempts each).",
|
|
1488
|
+
MAX_TERMINAL_RESTART_CYCLES,
|
|
1489
|
+
max_retries,
|
|
1490
|
+
)
|
|
1491
|
+
raise ThetaDataConnectionError(
|
|
1492
|
+
f"Unable to connect to Theta Data after {MAX_TERMINAL_RESTART_CYCLES} restart cycle(s)."
|
|
1493
|
+
)
|
|
1471
1494
|
|
|
1472
|
-
|
|
1495
|
+
logger.warning(
|
|
1496
|
+
"ThetaTerminal still disconnected after %s attempts; restarting (cycle %s/%s).",
|
|
1497
|
+
max_retries,
|
|
1498
|
+
total_restart_cycles,
|
|
1499
|
+
MAX_TERMINAL_RESTART_CYCLES,
|
|
1500
|
+
)
|
|
1501
|
+
client = start_theta_data_client(username=username, password=password)
|
|
1502
|
+
CONNECTION_DIAGNOSTICS["terminal_restarts"] = CONNECTION_DIAGNOSTICS.get("terminal_restarts", 0) + 1
|
|
1503
|
+
time.sleep(max(BOOT_GRACE_PERIOD, sleep_interval))
|
|
1473
1504
|
|
|
1474
1505
|
|
|
1475
1506
|
def get_request(url: str, headers: dict, querystring: dict, username: str, password: str):
|
|
1476
1507
|
all_responses = []
|
|
1477
1508
|
next_page_url = None
|
|
1478
1509
|
page_count = 0
|
|
1510
|
+
consecutive_disconnects = 0
|
|
1511
|
+
restart_budget = 3
|
|
1479
1512
|
|
|
1480
1513
|
# Lightweight liveness probe before issuing the request
|
|
1481
1514
|
check_connection(username=username, password=password, wait_for_connection=False)
|
|
@@ -1498,25 +1531,52 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
|
|
|
1498
1531
|
)
|
|
1499
1532
|
|
|
1500
1533
|
response = requests.get(request_url, headers=headers, params=request_params)
|
|
1534
|
+
status_code = response.status_code
|
|
1501
1535
|
# Status code 472 means "No data" - this is valid, return None
|
|
1502
|
-
if
|
|
1536
|
+
if status_code == 472:
|
|
1503
1537
|
logger.warning(f"No data available for request: {response.text[:200]}")
|
|
1504
1538
|
# DEBUG-LOG: API response - no data
|
|
1505
1539
|
logger.debug(
|
|
1506
1540
|
"[THETA][DEBUG][API][RESPONSE] status=472 result=NO_DATA"
|
|
1507
1541
|
)
|
|
1542
|
+
consecutive_disconnects = 0
|
|
1508
1543
|
return None
|
|
1544
|
+
elif status_code == 474:
|
|
1545
|
+
consecutive_disconnects += 1
|
|
1546
|
+
logger.warning("Received 474 from Theta Data (attempt %s): %s", counter + 1, response.text[:200])
|
|
1547
|
+
if consecutive_disconnects >= 2:
|
|
1548
|
+
if restart_budget <= 0:
|
|
1549
|
+
logger.error("Restart budget exhausted after repeated 474 responses.")
|
|
1550
|
+
raise ValueError("Cannot connect to Theta Data!")
|
|
1551
|
+
logger.warning(
|
|
1552
|
+
"Restarting ThetaTerminal after %s consecutive 474 responses (restart budget remaining %s).",
|
|
1553
|
+
consecutive_disconnects,
|
|
1554
|
+
restart_budget - 1,
|
|
1555
|
+
)
|
|
1556
|
+
restart_budget -= 1
|
|
1557
|
+
start_theta_data_client(username=username, password=password)
|
|
1558
|
+
CONNECTION_DIAGNOSTICS["terminal_restarts"] = CONNECTION_DIAGNOSTICS.get("terminal_restarts", 0) + 1
|
|
1559
|
+
check_connection(username=username, password=password, wait_for_connection=True)
|
|
1560
|
+
time.sleep(max(BOOT_GRACE_PERIOD, CONNECTION_RETRY_SLEEP))
|
|
1561
|
+
consecutive_disconnects = 0
|
|
1562
|
+
counter = 0
|
|
1563
|
+
else:
|
|
1564
|
+
check_connection(username=username, password=password, wait_for_connection=True)
|
|
1565
|
+
time.sleep(CONNECTION_RETRY_SLEEP)
|
|
1566
|
+
continue
|
|
1509
1567
|
# If status code is not 200, then we are not connected
|
|
1510
|
-
elif
|
|
1511
|
-
logger.warning(f"Non-200 status code {
|
|
1568
|
+
elif status_code != 200:
|
|
1569
|
+
logger.warning(f"Non-200 status code {status_code}: {response.text[:200]}")
|
|
1512
1570
|
# DEBUG-LOG: API response - error
|
|
1513
1571
|
logger.debug(
|
|
1514
1572
|
"[THETA][DEBUG][API][RESPONSE] status=%d result=ERROR",
|
|
1515
|
-
|
|
1573
|
+
status_code
|
|
1516
1574
|
)
|
|
1517
1575
|
check_connection(username=username, password=password, wait_for_connection=True)
|
|
1576
|
+
consecutive_disconnects = 0
|
|
1518
1577
|
else:
|
|
1519
1578
|
json_resp = response.json()
|
|
1579
|
+
consecutive_disconnects = 0
|
|
1520
1580
|
|
|
1521
1581
|
# DEBUG-LOG: API response - success
|
|
1522
1582
|
response_rows = len(json_resp.get("response", [])) if isinstance(json_resp.get("response"), list) else 0
|
|
@@ -1542,6 +1602,9 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
|
|
|
1542
1602
|
else:
|
|
1543
1603
|
break
|
|
1544
1604
|
|
|
1605
|
+
except ThetaDataConnectionError as exc:
|
|
1606
|
+
logger.error("Theta Data connection failed after supervised restarts: %s", exc)
|
|
1607
|
+
raise
|
|
1545
1608
|
except Exception as e:
|
|
1546
1609
|
logger.warning(f"Exception during request (attempt {counter + 1}): {e}")
|
|
1547
1610
|
check_connection(username=username, password=password, wait_for_connection=True)
|
|
@@ -1551,7 +1614,7 @@ def get_request(url: str, headers: dict, querystring: dict, username: str, passw
|
|
|
1551
1614
|
|
|
1552
1615
|
counter += 1
|
|
1553
1616
|
if counter > 1:
|
|
1554
|
-
raise
|
|
1617
|
+
raise ThetaDataConnectionError("Unable to connect to Theta Data after repeated retries.")
|
|
1555
1618
|
|
|
1556
1619
|
# Store this page's response data
|
|
1557
1620
|
page_count += 1
|
|
@@ -132,7 +132,7 @@ lumibot/tools/polygon_helper_async.py,sha256=YHDXa9kmkkn8jh7hToY6GP5etyXS9Tj-uky
|
|
|
132
132
|
lumibot/tools/polygon_helper_polars_optimized.py,sha256=NaIZ-5Av-G2McPEKHyJ-x65W72W_Agnz4lRgvXfQp8c,30415
|
|
133
133
|
lumibot/tools/projectx_helpers.py,sha256=EIemLfbG923T_RBV_i6s6A9xgs7dt0et0oCnhFwdWfA,58299
|
|
134
134
|
lumibot/tools/schwab_helper.py,sha256=CXnYhgsXOIb5MgmIYOp86aLxsBF9oeVrMGrjwl_GEv0,11768
|
|
135
|
-
lumibot/tools/thetadata_helper.py,sha256=
|
|
135
|
+
lumibot/tools/thetadata_helper.py,sha256=F5CdaWZkfJYFrParvN8KhowRWc5cRmPL4TnI9SH-VpI,90111
|
|
136
136
|
lumibot/tools/types.py,sha256=x-aQBeC6ZTN2-pUyxyo69Q0j5e0c_swdfe06kfrWSVc,1978
|
|
137
137
|
lumibot/tools/yahoo_helper.py,sha256=htcKKkuktatIckVKfLc_ms0X75mXColysQhrZW244z8,19497
|
|
138
138
|
lumibot/tools/yahoo_helper_polars_optimized.py,sha256=g9xBN-ReHSW4Aj9EMU_OncBXVS1HpfL8LTHit9ZxFY4,7417
|
|
@@ -142,7 +142,7 @@ lumibot/traders/trader.py,sha256=KMif3WoZtnSxA0BzoK3kvkTITNELrDFIortx1BYBv8s,967
|
|
|
142
142
|
lumibot/trading_builtins/__init__.py,sha256=vH2QL5zLjL3slfEV1YW-BvQHtEYLCFkIWTZDfh3y8LE,87
|
|
143
143
|
lumibot/trading_builtins/custom_stream.py,sha256=8_XiPT0JzyXrgnXCXoovGGUrWEfnG4ohIYMPfB_Nook,5264
|
|
144
144
|
lumibot/trading_builtins/safe_list.py,sha256=IIjZOHSiZYK25A4WBts0oJaZNOJDsjZL65MOSHhE3Ig,1975
|
|
145
|
-
lumibot-4.2.
|
|
145
|
+
lumibot-4.2.9.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
|
|
146
146
|
tests/__init__.py,sha256=3-VoT-nAuqMfwufd4ceN6fXaHl_zCfDCSXJOTp1ywYQ,393
|
|
147
147
|
tests/conftest.py,sha256=UBw_2fx7r6TZPKus2b1Qxrzmd4bg8EEBnX1vCHUuSVA,3311
|
|
148
148
|
tests/fixtures.py,sha256=wOHQsh1SGHnXe_PGi6kDWI30CS_Righi7Ig7vwSEKT4,9082
|
|
@@ -236,7 +236,7 @@ tests/test_session_manager.py,sha256=1qygN3aQ2Xe2uh4BMPm0E3V8KXLFNGq5qdL8KkZjef4
|
|
|
236
236
|
tests/test_strategy_methods.py,sha256=j9Mhr6nnG1fkiVQXnx7gLjzGbeQmwt0UbJr_4plD36o,12539
|
|
237
237
|
tests/test_strategy_price_guard.py,sha256=3GJdlfROwx6-adsSi8ZBrWaLOy9e-0N6V1eqpikj8e4,1540
|
|
238
238
|
tests/test_thetadata_backwards_compat.py,sha256=RzNLhNZNJZ2hPkEDyG-T_4mRRXh5XqavK6r-OjfRASQ,3306
|
|
239
|
-
tests/test_thetadata_helper.py,sha256=
|
|
239
|
+
tests/test_thetadata_helper.py,sha256=4EdFHoCTTKELU216ZNYGjfo6Uvq-29kK-TVpFLPWT3M,67263
|
|
240
240
|
tests/test_thetadata_pandas_verification.py,sha256=MWUecqBY6FGFslWLRo_C5blGbom_unmXCZikAfZXLks,6553
|
|
241
241
|
tests/test_tradier.py,sha256=iCEM2FTxJSzJ2oLNaRqSx05XaX_DCiMzLx1aEYPANko,33280
|
|
242
242
|
tests/test_tradier_data.py,sha256=1jTxDzQtzaC42CQJVXMRMElBwExy1mVci3NFfKjjVH0,13363
|
|
@@ -282,7 +282,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
|
|
|
282
282
|
tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
|
|
283
283
|
tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
|
|
284
284
|
tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
|
|
285
|
-
lumibot-4.2.
|
|
286
|
-
lumibot-4.2.
|
|
287
|
-
lumibot-4.2.
|
|
288
|
-
lumibot-4.2.
|
|
285
|
+
lumibot-4.2.9.dist-info/METADATA,sha256=Bis7WU6RY3Wp7YXHsNa7-xklQzQcXRi5zcJzZhC2j0w,12093
|
|
286
|
+
lumibot-4.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
287
|
+
lumibot-4.2.9.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
|
|
288
|
+
lumibot-4.2.9.dist-info/RECORD,,
|
tests/test_thetadata_helper.py
CHANGED
|
@@ -1426,8 +1426,10 @@ class TestThetaDataChainsCaching:
|
|
|
1426
1426
|
|
|
1427
1427
|
monkeypatch.setattr(thetadata_helper, "build_historical_chain", lambda **kwargs: None)
|
|
1428
1428
|
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
|
|
1429
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1430
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1429
1431
|
|
|
1430
|
-
with caplog.at_level(logging.WARNING):
|
|
1432
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1431
1433
|
result = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1432
1434
|
|
|
1433
1435
|
cache_folder = Path(tmp_path) / "thetadata" / "stock" / "option_chains"
|
|
@@ -1508,8 +1510,10 @@ def test_build_historical_chain_returns_none_when_no_dates(monkeypatch, caplog):
|
|
|
1508
1510
|
raise AssertionError(f"Unexpected URL {url}")
|
|
1509
1511
|
|
|
1510
1512
|
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1513
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1514
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1511
1515
|
|
|
1512
|
-
with caplog.at_level(logging.WARNING):
|
|
1516
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1513
1517
|
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1514
1518
|
|
|
1515
1519
|
assert result is None
|
|
@@ -1525,14 +1529,83 @@ def test_build_historical_chain_empty_response(monkeypatch, caplog):
|
|
|
1525
1529
|
raise AssertionError("Unexpected call after empty expirations")
|
|
1526
1530
|
|
|
1527
1531
|
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1532
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1533
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1528
1534
|
|
|
1529
|
-
with caplog.at_level(logging.WARNING):
|
|
1535
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1530
1536
|
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1531
1537
|
|
|
1532
1538
|
assert result is None
|
|
1533
1539
|
assert "returned no expirations" in caplog.text
|
|
1534
1540
|
|
|
1535
1541
|
|
|
1542
|
+
class TestThetaDataConnectionSupervision:
|
|
1543
|
+
|
|
1544
|
+
def setup_method(self):
|
|
1545
|
+
thetadata_helper.reset_connection_diagnostics()
|
|
1546
|
+
|
|
1547
|
+
def test_check_connection_recovers_after_restart(self, monkeypatch):
|
|
1548
|
+
statuses = iter(["DISCONNECTED", "DISCONNECTED", "CONNECTED"])
|
|
1549
|
+
|
|
1550
|
+
class FakeResponse:
|
|
1551
|
+
def __init__(self, text):
|
|
1552
|
+
self.text = text
|
|
1553
|
+
|
|
1554
|
+
def fake_get(url, timeout):
|
|
1555
|
+
try:
|
|
1556
|
+
text = next(statuses)
|
|
1557
|
+
except StopIteration:
|
|
1558
|
+
text = "CONNECTED"
|
|
1559
|
+
return FakeResponse(text)
|
|
1560
|
+
|
|
1561
|
+
start_calls = []
|
|
1562
|
+
|
|
1563
|
+
def fake_start(username, password):
|
|
1564
|
+
start_calls.append((username, password))
|
|
1565
|
+
return object()
|
|
1566
|
+
|
|
1567
|
+
monkeypatch.setattr(thetadata_helper.requests, "get", fake_get)
|
|
1568
|
+
monkeypatch.setattr(thetadata_helper, "start_theta_data_client", fake_start)
|
|
1569
|
+
monkeypatch.setattr(thetadata_helper, "is_process_alive", lambda: True)
|
|
1570
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_MAX_RETRIES", 2, raising=False)
|
|
1571
|
+
monkeypatch.setattr(thetadata_helper, "MAX_TERMINAL_RESTART_CYCLES", 2, raising=False)
|
|
1572
|
+
monkeypatch.setattr(thetadata_helper, "BOOT_GRACE_PERIOD", 0, raising=False)
|
|
1573
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_RETRY_SLEEP", 0, raising=False)
|
|
1574
|
+
monkeypatch.setattr(thetadata_helper.time, "sleep", lambda *args, **kwargs: None)
|
|
1575
|
+
|
|
1576
|
+
client, connected = thetadata_helper.check_connection("user", "pass", wait_for_connection=True)
|
|
1577
|
+
|
|
1578
|
+
assert connected is True
|
|
1579
|
+
assert len(start_calls) == 1
|
|
1580
|
+
assert thetadata_helper.CONNECTION_DIAGNOSTICS["terminal_restarts"] >= 1
|
|
1581
|
+
|
|
1582
|
+
def test_check_connection_raises_after_restart_cycles(self, monkeypatch):
|
|
1583
|
+
statuses = iter(["DISCONNECTED"] * 10)
|
|
1584
|
+
|
|
1585
|
+
class FakeResponse:
|
|
1586
|
+
def __init__(self, text):
|
|
1587
|
+
self.text = text
|
|
1588
|
+
|
|
1589
|
+
def fake_get(url, timeout):
|
|
1590
|
+
try:
|
|
1591
|
+
text = next(statuses)
|
|
1592
|
+
except StopIteration:
|
|
1593
|
+
text = "DISCONNECTED"
|
|
1594
|
+
return FakeResponse(text)
|
|
1595
|
+
|
|
1596
|
+
monkeypatch.setattr(thetadata_helper.requests, "get", fake_get)
|
|
1597
|
+
monkeypatch.setattr(thetadata_helper, "start_theta_data_client", lambda *args, **kwargs: object())
|
|
1598
|
+
monkeypatch.setattr(thetadata_helper, "is_process_alive", lambda: True)
|
|
1599
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_MAX_RETRIES", 1, raising=False)
|
|
1600
|
+
monkeypatch.setattr(thetadata_helper, "MAX_TERMINAL_RESTART_CYCLES", 1, raising=False)
|
|
1601
|
+
monkeypatch.setattr(thetadata_helper, "BOOT_GRACE_PERIOD", 0, raising=False)
|
|
1602
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_RETRY_SLEEP", 0, raising=False)
|
|
1603
|
+
monkeypatch.setattr(thetadata_helper.time, "sleep", lambda *args, **kwargs: None)
|
|
1604
|
+
|
|
1605
|
+
with pytest.raises(thetadata_helper.ThetaDataConnectionError):
|
|
1606
|
+
thetadata_helper.check_connection("user", "pass", wait_for_connection=True)
|
|
1607
|
+
|
|
1608
|
+
|
|
1536
1609
|
def test_finalize_day_frame_handles_dst_fallback():
|
|
1537
1610
|
tz = pytz.timezone("America/New_York")
|
|
1538
1611
|
utc = pytz.UTC
|
|
File without changes
|
|
File without changes
|
|
File without changes
|