import json, os, sqlite3, time
from http.client import IncompleteRead
from urllib.error import HTTPError
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from urllib.request import Request, urlopen
URL = os.environ["LASSO_STREAM_URL"]
SOURCE = os.environ.get("STREAM_ID", "contract-logs")
START = os.environ["START_BLOCK"]
db = sqlite3.connect("streams.sqlite")
db.executescript("""
CREATE TABLE IF NOT EXISTS items (source TEXT, block INTEGER, payload TEXT);
CREATE TABLE IF NOT EXISTS positions (source TEXT PRIMARY KEY, next TEXT);
""")
def request_url():
u = urlsplit(URL)
q = dict(parse_qsl(u.query))
row = db.execute("SELECT next FROM positions WHERE source=?", (SOURCE,)).fetchone()
q["from"] = row[0] if row else START
q["wait"] = "20"
return urlunsplit((u.scheme, u.netloc, u.path, urlencode(q), ""))
def apply_page(page):
with db:
db.execute("DELETE FROM items WHERE source=? AND block>=?", (SOURCE, page["from"]))
db.executemany("INSERT INTO items VALUES (?,?,?)", [
(SOURCE, int(item.get("blockNumber", item.get("number")), 16), json.dumps(item))
for item in page["items"]
])
db.execute("INSERT OR REPLACE INTO positions VALUES (?,?)", (SOURCE, page["next"]))
def rebuild_source():
with db:
db.execute("DELETE FROM items WHERE source=?", (SOURCE,))
db.execute("DELETE FROM positions WHERE source=?", (SOURCE,))
def handle_http(error):
if error.code == 409:
rebuild_source() # rebuild only this source from START
elif error.code in (307, 429) or error.code >= 500:
time.sleep(int(error.headers.get("Retry-After", "2")))
else:
raise SystemExit(f"Streams HTTP {error.code}; inspect the response privately")
def handle_control(body):
error = body["error"]
code = error["code"] if isinstance(error, dict) else error
if code == "resync_required":
rebuild_source()
elif code in ("chain_unavailable", "too_many_readers", "rate_limit_exceeded"):
time.sleep(body.get("retry_after", 2)) # keep the saved from
else:
raise SystemExit(f"Streams {code}; inspect the control body privately")
def pull():
while True:
request = Request(request_url())
try:
with urlopen(request, timeout=40) as response:
page = json.load(response)
except HTTPError as error:
handle_http(error)
continue
except (OSError, IncompleteRead, json.JSONDecodeError):
time.sleep(2)
continue # an incomplete page never reaches the transaction
apply_page(page) # database and apply errors remain fatal
if __name__ == "__main__":
pull()