import unittest
import pytest
CONN_STR = "host=localhost port=5432 user=postgres password=postgres dbname=postgres"
UNREACHABLE = "host=127.0.0.1 port=1 user=postgres dbname=postgres connect_timeout=1"
KDB_EPOCH_SECS = 946684800.0
ONE_DAY = 86400.0
class TestPostgresConstruction(unittest.TestCase):
def test_read_constructs_stream(self):
from wingfoil import postgres_read
stream = postgres_read(
UNREACHABLE,
"SELECT time, sym FROM trades",
"time",
chunk_size=3600,
)
self.assertIsNotNone(stream)
def test_write_method_constructs_node(self):
from wingfoil import constant
node = constant({"sym": "A", "price": 1.0, "qty": 1}).postgres_write(
UNREACHABLE,
"trades",
[("sym", "text"), ("price", "float"), ("qty", "int")],
)
self.assertIsNotNone(node)
class TestPostgresUnreachable(unittest.TestCase):
def test_write_single_dict_marshals_then_errors(self):
from wingfoil import constant
node = constant({"sym": "A", "price": 1.0, "qty": 1}).postgres_write(
UNREACHABLE,
"trades",
[("sym", "text"), ("price", "float"), ("qty", "int")],
)
with self.assertRaises(Exception):
node.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_write_list_of_dicts_marshals_then_errors(self):
from wingfoil import constant
rows = [
{"sym": "A", "price": 1.0, "qty": 1},
{"sym": "B", "price": 2.0, "qty": 2},
]
node = constant(rows).postgres_write(
UNREACHABLE,
"trades",
[("sym", "text"), ("price", "float"), ("qty", "int")],
)
with self.assertRaises(Exception):
node.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_write_bad_value_type_errors(self):
from wingfoil import constant
node = constant("not a dict").postgres_write(
UNREACHABLE, "trades", [("sym", "text")]
)
with self.assertRaises(Exception):
node.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_write_missing_key_errors(self):
from wingfoil import constant
node = constant({"sym": "A"}).postgres_write(
UNREACHABLE, "trades", [("sym", "text"), ("price", "float")]
)
with self.assertRaises(Exception):
node.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_write_unsupported_type_errors(self):
from wingfoil import constant
node = constant({"price": 1.0}).postgres_write(
UNREACHABLE, "trades", [("price", "flaot")]
)
with self.assertRaises(Exception):
node.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_write_wrong_value_type_errors(self):
from wingfoil import constant
node = constant({"qty": "not an int"}).postgres_write(
UNREACHABLE, "trades", [("qty", "long")]
)
with self.assertRaises(Exception):
node.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_read_unreachable_errors(self):
from wingfoil import postgres_read
stream = postgres_read(
UNREACHABLE, "SELECT time, sym FROM trades", "time"
).collect()
with self.assertRaises(Exception):
stream.run(realtime=False, start=KDB_EPOCH_SECS, duration=ONE_DAY)
def test_read_zero_chunk_size_errors(self):
from wingfoil import postgres_read
stream = postgres_read(
UNREACHABLE, "SELECT time, sym FROM trades", "time", chunk_size=0
).collect()
with self.assertRaises(Exception):
stream.run(realtime=False, start=KDB_EPOCH_SECS, duration=ONE_DAY)
class TestPostgresSub(unittest.TestCase):
def test_sub_constructs_stream(self):
from wingfoil import postgres_sub
stream = postgres_sub(
UNREACHABLE, "SELECT time, sym FROM trades", "time", "trades_feed"
)
self.assertIsNotNone(stream)
def test_sub_rejects_historical_mode(self):
from wingfoil import postgres_sub
stream = postgres_sub(
UNREACHABLE, "SELECT time, sym FROM trades", "time", "trades_feed"
).collect()
with self.assertRaises(Exception):
stream.run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
def test_sub_unreachable_errors(self):
from wingfoil import postgres_sub
stream = postgres_sub(
UNREACHABLE, "SELECT time, sym FROM trades", "time", "trades_feed"
).collect()
with self.assertRaises(Exception):
stream.run(realtime=True, duration=2.0)
def test_notify_trigger_sql_shape(self):
from wingfoil import postgres_notify_trigger_sql
sql = postgres_notify_trigger_sql("trades", "trades_feed")
self.assertIn("pg_notify('trades_feed', '')", sql)
self.assertIn('AFTER INSERT ON "trades"', sql)
def _reset_table(sql_rows=()):
import psycopg
with psycopg.connect(CONN_STR, autocommit=True) as conn:
conn.execute("DROP TABLE IF EXISTS py_pg_trades")
conn.execute(
"CREATE TABLE py_pg_trades "
"(time timestamp, sym text, price float8, qty int8)"
)
for ts, sym, price, qty in sql_rows:
conn.execute(
"INSERT INTO py_pg_trades VALUES (%s, %s, %s, %s)",
(ts, sym, price, qty),
)
@pytest.mark.requires_postgres
class TestPostgresRead(unittest.TestCase):
def test_read_returns_dicts(self):
from wingfoil import postgres_read
_reset_table(
[
("2000-01-01 00:00:01", "AAPL", 100.0, 10),
("2000-01-01 00:00:02", "GOOG", 200.0, 20),
("2000-01-01 00:00:03", "MSFT", 300.0, 30),
]
)
stream = postgres_read(
CONN_STR,
"SELECT time, sym, price, qty FROM py_pg_trades",
"time",
chunk_size=86400,
).collect()
stream.run(realtime=False, start=KDB_EPOCH_SECS, duration=ONE_DAY)
rows = stream.peek_value()
self.assertIsInstance(rows, list)
self.assertEqual(len(rows), 3)
self.assertIsInstance(rows[0], dict)
self.assertEqual({r["sym"] for r in rows}, {"AAPL", "GOOG", "MSFT"})
self.assertEqual(rows[0]["qty"], 10)
@pytest.mark.requires_postgres
class TestPostgresWrite(unittest.TestCase):
def test_write_round_trip(self):
import psycopg
from wingfoil import constant
_reset_table()
constant({"sym": "TEST", "price": 42.0, "qty": 100}).postgres_write(
CONN_STR,
"py_pg_trades",
[("sym", "text"), ("price", "float"), ("qty", "long")],
).run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
with psycopg.connect(CONN_STR) as conn:
row = conn.execute(
"SELECT sym, price, qty FROM py_pg_trades"
).fetchone()
self.assertEqual(row[0], "TEST")
self.assertAlmostEqual(row[1], 42.0)
self.assertEqual(row[2], 100)
def test_write_int4_and_null_round_trip(self):
import psycopg
from wingfoil import constant, postgres_read
with psycopg.connect(CONN_STR, autocommit=True) as conn:
conn.execute("DROP TABLE IF EXISTS py_pg_int4")
conn.execute(
"CREATE TABLE py_pg_int4 (time timestamp, qty integer, note text)"
)
constant({"qty": 7, "note": None}).postgres_write(
CONN_STR,
"py_pg_int4",
[("qty", "int"), ("note", "text")],
).run(realtime=False, start=KDB_EPOCH_SECS, cycles=1)
with psycopg.connect(CONN_STR) as conn:
row = conn.execute("SELECT qty, note FROM py_pg_int4").fetchone()
self.assertEqual(row[0], 7)
self.assertIsNone(row[1])
stream = postgres_read(
CONN_STR, "SELECT time, qty, note FROM py_pg_int4", "time", chunk_size=86400
).collect()
stream.run(realtime=False, start=KDB_EPOCH_SECS, duration=ONE_DAY)
rows = stream.peek_value()
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["qty"], 7)
self.assertIsNone(rows[0]["note"])
@pytest.mark.requires_postgres
class TestPostgresSubLive(unittest.TestCase):
def test_sub_catch_up_then_live_inserts(self):
import threading
import time as time_mod
import psycopg
from wingfoil import postgres_notify_trigger_sql, postgres_sub
_reset_table(
[("2000-01-01 00:00:00", "SEED0", 1.0, 10)]
)
with psycopg.connect(CONN_STR, autocommit=True) as conn:
conn.execute(postgres_notify_trigger_sql("py_pg_trades", "py_pg_feed"))
def insert_later():
time_mod.sleep(0.5)
with psycopg.connect(CONN_STR, autocommit=True) as conn:
conn.execute(
"INSERT INTO py_pg_trades VALUES "
"('2000-01-01 00:00:01', 'LIVE1', 2.0, 20)"
)
conn.execute(
"INSERT INTO py_pg_trades VALUES "
"('2000-01-01 00:00:02', 'LIVE2', 3.0, 30)"
)
inserter = threading.Thread(target=insert_later)
inserter.start()
stream = postgres_sub(
CONN_STR,
"SELECT time, sym, price, qty FROM py_pg_trades",
"time",
"py_pg_feed",
start=0.0, ).collect()
stream.run(realtime=True, duration=3.0)
inserter.join()
ticks = stream.peek_value()
syms = [row["sym"] for tick in ticks for row in tick]
self.assertEqual(syms, ["SEED0", "LIVE1", "LIVE2"])