Skip to content

Commit

Permalink
[replit] The lesser of two evils
Browse files Browse the repository at this point in the history
Currently the replit library has a very gross quirk: it has a global in
`replit.database.default_db.db`, and the mere action of importing this
library causes side effects to run! (connects to the database, starts a
thread to refresh the URL, and prints a warning to stdout, adding insult
to injury).

So we're trading that very gross quirk with a gross workaround to
preserve backwards compatibility: the modules that somehow end up
importing that module now have a `__getattr__` that _lazily_ calls the
code that used to be invoked as a side-effect of importing the library.
Maybe in the future we'll deploy a breaking version of the library where
we're not beholden to this backwards-compatibility quirck.
  • Loading branch information
lhchavez committed Sep 6, 2023
1 parent 2a39338 commit 582dafb
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 34 deletions.
13 changes: 12 additions & 1 deletion src/replit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

"""The Replit Python module."""

from typing import Any

from . import web
from .audio import Audio
from .database import (
db,
LazyDB,
Database,
AsyncDatabase,
make_database_proxy_blueprint,
Expand All @@ -23,3 +25,12 @@ def clear() -> None:


audio = Audio()

# Previous versions of this library would just have side-effects and always set
# up a database unconditionally. That is very undesirable, so instead of doing
# that, we are using this egregious hack to get the database / database URL
# lazily.
def __getattr__(name: str) -> Any:
if name == "db":
return LazyDB.get_instance().db
raise AttributeError(name)
16 changes: 15 additions & 1 deletion src/replit/database/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Interface with the Replit Database."""
from typing import Any

from .database import AsyncDatabase, Database, DBJSONEncoder, dumps, to_primitive
from .default_db import db, db_url
from .default_db import LazyDB
from .server import make_database_proxy_blueprint, start_database_proxy

__all__ = [
Expand All @@ -14,3 +16,15 @@
"start_database_proxy",
"to_primitive",
]


# Previous versions of this library would just have side-effects and always set
# up a database unconditionally. That is very undesirable, so instead of doing
# that, we are using this egregious hack to get the database / database URL
# lazily.
def __getattr__(name: str) -> Any:
if name == "db":
return LazyDB.get_instance().db
if name == "db_url":
return LazyDB.get_instance().db_url
raise AttributeError(name)
71 changes: 47 additions & 24 deletions src/replit/database/default_db.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,64 @@
"""A module containing the default database."""
from os import environ, path
import logging
import os
import os.path
import threading
from typing import Optional
from typing import Any, Optional


from .database import Database


def get_db_url() -> str:
def get_db_url() -> Optional[str]:
"""Fetches the most up-to-date db url from the Repl environment."""
# todo look into the security warning ignored below
tmpdir = "/tmp/replitdb" # noqa: S108
if path.exists(tmpdir):
if os.path.exists(tmpdir):
with open(tmpdir, "r") as file:
db_url = file.read()
else:
db_url = environ.get("REPLIT_DB_URL")
return file.read()

return db_url
return os.environ.get("REPLIT_DB_URL")


def refresh_db() -> None:
"""Refresh the DB URL every hour."""
global db
db_url = get_db_url()
db.update_db_url(db_url)
threading.Timer(3600, refresh_db).start()
class LazyDB:
"""A way to lazily create a database connection."""

_instance: Optional["LazyDB"] = None

db: Optional[Database]
db_url = get_db_url()
if db_url:
db = Database(db_url)
else:
# The user will see errors if they try to use the database.
print("Warning: error initializing database. Replit DB is not configured.")
db = None
def __init__(self) -> None:
self.db: Optional[Database] = None
self.db_url = get_db_url()
if self.db_url:
self.db = Database(self.db_url)
self.refresh_db()
else:
logging.warning(
"Warning: error initializing database. Replit DB is not configured."
)

if db:
refresh_db()
def refresh_db(self) -> None:
"""Refresh the DB URL every hour."""
if not self.db:
return
self.db_url = get_db_url()
if self.db_url:
self.db.update_db_url(self.db_url)
threading.Timer(3600, self.refresh_db).start()

@classmethod
def get_instance(cls) -> "LazyDB":
if cls._instance is None:
cls._instance = LazyDB()
return cls._instance


# Previous versions of this library would just have side-effects and always set
# up a database unconditionally. That is very undesirable, so instead of doing
# that, we are using this egregious hack to get the database / database URL
# lazily.
def __getattr__(name: str) -> Any:
if name == "db":
return LazyDB.get_instance().db
if name == "db_url":
return LazyDB.get_instance().db_url
raise AttributeError(name)
20 changes: 16 additions & 4 deletions src/replit/database/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from flask import Blueprint, Flask, request

from .default_db import db
from .default_db import LazyDB


def make_database_proxy_blueprint(view_only: bool, prefix: str = "") -> Blueprint:
Expand All @@ -20,17 +20,23 @@ def make_database_proxy_blueprint(view_only: bool, prefix: str = "") -> Blueprin
app = Blueprint("database_proxy" + ("_view_only" if view_only else ""), __name__)

def list_keys() -> Any:
user_prefix = request.args.get("prefix")
db = LazyDB.get_instance().db
if db is None:
return "Database is not configured", 500
user_prefix = request.args.get("prefix", "")
encode = "encode" in request.args
keys = db.prefix(prefix=prefix + user_prefix)
keys = [k[len(prefix) :] for k in keys]
raw_keys = db.prefix(prefix=prefix + user_prefix)
keys = [k[len(prefix) :] for k in raw_keys]

if encode:
return "\n".join(quote(k) for k in keys)
else:
return "\n".join(keys)

def set_key() -> Any:
db = LazyDB.get_instance().db
if db is None:
return "Database is not configured", 500
if view_only:
return "Database is view only", 401
for k, v in request.form.items():
Expand All @@ -44,12 +50,18 @@ def index() -> Any:
return set_key()

def get_key(key: str) -> Any:
db = LazyDB.get_instance().db
if db is None:
return "Database is not configured", 500
try:
return db[prefix + key]
except KeyError:
return "", 404

def delete_key(key: str) -> Any:
db = LazyDB.get_instance().db
if db is None:
return "Database is not configured", 500
if view_only:
return "Database is view only", 401
try:
Expand Down
11 changes: 10 additions & 1 deletion src/replit/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
from .app import debug, ReplitAuthContext, run
from .user import User, UserStore
from .utils import *
from ..database import AsyncDatabase, Database, db
from ..database import AsyncDatabase, Database, LazyDB

auth = LocalProxy(lambda: ReplitAuthContext.from_headers(flask.request.headers))

# Previous versions of this library would just have side-effects and always set
# up a database unconditionally. That is very undesirable, so instead of doing
# that, we are using this egregious hack to get the database / database URL
# lazily.
def __getattr__(name: str) -> Any:
if name == "db":
return LazyDB.get_instance().db
raise AttributeError(name)
13 changes: 10 additions & 3 deletions src/replit/web/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import flask

from .app import ReplitAuthContext
from ..database import Database, db as real_db

db: Database = real_db # type: ignore
from ..database import LazyDB


class User(MutableMapping):
Expand All @@ -32,9 +30,15 @@ def set_value(self, value: str) -> None:
Args:
value (str): The value to set in the database
"""
db = LazyDB.get_instance().db
if db is None:
raise RuntimeError("database not configured")
db[self.db_key()] = value

def _ensure_value(self) -> Any:
db = LazyDB.get_instance().db
if db is None:
raise RuntimeError("database not configured")
try:
return db[self.db_key()]
except KeyError:
Expand Down Expand Up @@ -103,6 +107,9 @@ def __getitem__(self, name: str) -> User:
return User(username=name, prefix=self.prefix)

def __iter__(self) -> Iterator[str]:
db = LazyDB.get_instance().db
if db is None:
raise RuntimeError("database not configured")
for k in db.keys():
if k.startswith(self.prefix):
yield self._strip_prefix(k)
Expand Down

0 comments on commit 582dafb

Please sign in to comment.