From 1aeeb18032a3e5e45df81c44f83130bc986a424b Mon Sep 17 00:00:00 2001 From: lhchavez Date: Wed, 6 Sep 2023 02:13:14 +0000 Subject: [PATCH 1/5] [replit] The lesser of two evils 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. --- src/replit/__init__.py | 14 +++++- src/replit/database/__init__.py | 16 ++++++- src/replit/database/default_db.py | 73 ++++++++++++++++++++----------- src/replit/database/server.py | 20 +++++++-- src/replit/web/__init__.py | 12 ++++- src/replit/web/user.py | 16 +++++-- 6 files changed, 116 insertions(+), 35 deletions(-) diff --git a/src/replit/__init__.py b/src/replit/__init__.py index 14f79cf9..515ffb3f 100644 --- a/src/replit/__init__.py +++ b/src/replit/__init__.py @@ -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, @@ -23,3 +25,13 @@ 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) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 2e94cc7a..0d44133f 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -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__ = [ @@ -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) diff --git a/src/replit/database/default_db.py b/src/replit/database/default_db.py index cc00c8d2..7b8c63f3 100644 --- a/src/replit/database/default_db.py +++ b/src/replit/database/default_db.py @@ -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 os.environ.get("REPLIT_DB_URL") + + +class LazyDB: + """A way to lazily create a database connection.""" - return db_url + _instance: Optional["LazyDB"] = 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." + ) -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() + 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": + """Get the lazy singleton instance.""" + if cls._instance is None: + cls._instance = LazyDB() + return cls._instance -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 -if db: - refresh_db() +# 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) diff --git a/src/replit/database/server.py b/src/replit/database/server.py index 0149415f..8b8fd99c 100644 --- a/src/replit/database/server.py +++ b/src/replit/database/server.py @@ -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: @@ -20,10 +20,13 @@ 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) @@ -31,6 +34,9 @@ def list_keys() -> Any: 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(): @@ -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: diff --git a/src/replit/web/__init__.py b/src/replit/web/__init__.py index 117b02f3..9ca57814 100644 --- a/src/replit/web/__init__.py +++ b/src/replit/web/__init__.py @@ -9,6 +9,16 @@ 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) diff --git a/src/replit/web/user.py b/src/replit/web/user.py index fa85ba87..c4bbe4a7 100644 --- a/src/replit/web/user.py +++ b/src/replit/web/user.py @@ -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): @@ -31,10 +29,19 @@ def set_value(self, value: str) -> None: Args: value (str): The value to set in the database + + Raises: + RuntimeError: Raised if the database is not configured. """ + 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: @@ -103,6 +110,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) From a34c32199dd54aefebca4bc381139e65e5bce0f6 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 24 Feb 2024 07:24:01 +0000 Subject: [PATCH 2/5] Marking internal properties as private Providing accessors, to hint that we are accessing mutable state --- src/replit/__init__.py | 2 +- src/replit/database/__init__.py | 4 ++-- src/replit/database/default_db.py | 30 ++++++++++++++++++++---------- src/replit/database/server.py | 8 ++++---- src/replit/web/__init__.py | 2 +- src/replit/web/user.py | 6 +++--- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/replit/__init__.py b/src/replit/__init__.py index 515ffb3f..805e49bd 100644 --- a/src/replit/__init__.py +++ b/src/replit/__init__.py @@ -33,5 +33,5 @@ def clear() -> None: # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_instance().db + return LazyDB.get_db() raise AttributeError(name) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 0d44133f..802e7fe3 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -24,7 +24,7 @@ # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_instance().db + return LazyDB.get_db() if name == "db_url": - return LazyDB.get_instance().db_url + return LazyDB.get_db_url() raise AttributeError(name) diff --git a/src/replit/database/default_db.py b/src/replit/database/default_db.py index 7b8c63f3..987004b0 100644 --- a/src/replit/database/default_db.py +++ b/src/replit/database/default_db.py @@ -25,10 +25,10 @@ class LazyDB: _instance: Optional["LazyDB"] = 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._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( @@ -37,11 +37,11 @@ def __init__(self) -> None: def refresh_db(self) -> None: """Refresh the DB URL every hour.""" - if not self.db: + if not self._db: return - self.db_url = get_db_url() - if self.db_url: - self.db.update_db_url(self.db_url) + 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 @@ -51,6 +51,16 @@ def get_instance(cls) -> "LazyDB": cls._instance = LazyDB() return cls._instance + @classmethod + def get_db(cls) -> Optional[Database]: + """Get a reference to the singleton Database instance.""" + return cls.get_instance()._db + + @classmethod + def get_db_url(cls) -> Optional[str]: + """Get the db_url connection string.""" + return cls.get_instance()._db_url + # 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 @@ -58,7 +68,7 @@ def get_instance(cls) -> "LazyDB": # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_instance().db + return LazyDB.get_db() if name == "db_url": - return LazyDB.get_instance().db_url + return LazyDB.get_db_url() raise AttributeError(name) diff --git a/src/replit/database/server.py b/src/replit/database/server.py index 8b8fd99c..237f7d3c 100644 --- a/src/replit/database/server.py +++ b/src/replit/database/server.py @@ -20,7 +20,7 @@ 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: - db = LazyDB.get_instance().db + db = LazyDB.get_db() if db is None: return "Database is not configured", 500 user_prefix = request.args.get("prefix", "") @@ -34,7 +34,7 @@ def list_keys() -> Any: return "\n".join(keys) def set_key() -> Any: - db = LazyDB.get_instance().db + db = LazyDB.get_db() if db is None: return "Database is not configured", 500 if view_only: @@ -50,7 +50,7 @@ def index() -> Any: return set_key() def get_key(key: str) -> Any: - db = LazyDB.get_instance().db + db = LazyDB.get_db() if db is None: return "Database is not configured", 500 try: @@ -59,7 +59,7 @@ def get_key(key: str) -> Any: return "", 404 def delete_key(key: str) -> Any: - db = LazyDB.get_instance().db + db = LazyDB.get_db() if db is None: return "Database is not configured", 500 if view_only: diff --git a/src/replit/web/__init__.py b/src/replit/web/__init__.py index 9ca57814..ec8e6ba2 100644 --- a/src/replit/web/__init__.py +++ b/src/replit/web/__init__.py @@ -20,5 +20,5 @@ # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_instance().db + return LazyDB.get_db() raise AttributeError(name) diff --git a/src/replit/web/user.py b/src/replit/web/user.py index c4bbe4a7..cb635369 100644 --- a/src/replit/web/user.py +++ b/src/replit/web/user.py @@ -33,13 +33,13 @@ def set_value(self, value: str) -> None: Raises: RuntimeError: Raised if the database is not configured. """ - db = LazyDB.get_instance().db + db = LazyDB.get_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 + db = LazyDB.get_db() if db is None: raise RuntimeError("database not configured") try: @@ -110,7 +110,7 @@ def __getitem__(self, name: str) -> User: return User(username=name, prefix=self.prefix) def __iter__(self) -> Iterator[str]: - db = LazyDB.get_instance().db + db = LazyDB.get_db() if db is None: raise RuntimeError("database not configured") for k in db.keys(): From e6284786bd9ded718b6c5bf10bedc12ef5d30e05 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 24 Feb 2024 07:56:15 +0000 Subject: [PATCH 3/5] Reintroduce refresh_db noop to avoid errors on upgrade --- src/replit/database/default_db.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/replit/database/default_db.py b/src/replit/database/default_db.py index 987004b0..5e080e6d 100644 --- a/src/replit/database/default_db.py +++ b/src/replit/database/default_db.py @@ -19,6 +19,11 @@ def get_db_url() -> Optional[str]: return os.environ.get("REPLIT_DB_URL") +def refresh_db() -> None: + """Deprecated: refresh_db is now located inside the LazyDB singleton instance.""" + pass + + class LazyDB: """A way to lazily create a database connection.""" From 5be6253cf442d2c97da63d80e00f3e7c8a3db196 Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Sat, 24 Feb 2024 09:07:54 +0000 Subject: [PATCH 4/5] Reflow LazyDB back down into default_db module An issue with LazyDB is that the refresh_db timer would not get canceled if the user closes the database. Additionally, the db_url refresh logic relies on injection, whereas the Database should ideally be the thing requesting that information from the environment --- src/replit/__init__.py | 5 +- src/replit/database/__init__.py | 6 +- src/replit/database/database.py | 96 ++++++++++++++++++++++++++++--- src/replit/database/default_db.py | 71 ++++++++--------------- src/replit/database/server.py | 22 +++---- src/replit/web/__init__.py | 5 +- src/replit/web/user.py | 21 +++---- 7 files changed, 140 insertions(+), 86 deletions(-) diff --git a/src/replit/__init__.py b/src/replit/__init__.py index 805e49bd..66f5ef4f 100644 --- a/src/replit/__init__.py +++ b/src/replit/__init__.py @@ -4,10 +4,9 @@ from typing import Any -from . import web +from . import database, web from .audio import Audio from .database import ( - LazyDB, Database, AsyncDatabase, make_database_proxy_blueprint, @@ -33,5 +32,5 @@ def clear() -> None: # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_db() + return database.db raise AttributeError(name) diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index 802e7fe3..2fafdec0 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -1,8 +1,8 @@ """Interface with the Replit Database.""" from typing import Any +from . import default_db from .database import AsyncDatabase, Database, DBJSONEncoder, dumps, to_primitive -from .default_db import LazyDB from .server import make_database_proxy_blueprint, start_database_proxy __all__ = [ @@ -24,7 +24,7 @@ # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_db() + return default_db.db if name == "db_url": - return LazyDB.get_db_url() + return default_db.db_url raise AttributeError(name) diff --git a/src/replit/database/database.py b/src/replit/database/database.py index f4e63399..025e6767 100644 --- a/src/replit/database/database.py +++ b/src/replit/database/database.py @@ -1,7 +1,8 @@ -"""Async and dict-like interfaces for interacting with Repl.it Database.""" +"""Async and dict-like interfaces for interacting with Replit Database.""" from collections import abc import json +import threading from typing import ( Any, Callable, @@ -61,24 +62,57 @@ def dumps(val: Any) -> str: class AsyncDatabase: - """Async interface for Repl.it Database.""" + """Async interface for Replit Database. - __slots__ = ("db_url", "sess", "client") + :param str db_url: The Database URL to connect to + :param int retry_count: How many retry attempts we should make + :param get_db_url Callable: A callback that returns the current db_url + :param unbind Callable: Permit additional behavior after Database close + """ + + __slots__ = ("db_url", "sess", "client", "_get_db_url", "_unbind", "_refresh_timer") + _refresh_timer: Optional[threading.Timer] - def __init__(self, db_url: str, retry_count: int = 5) -> None: + def __init__( + self, + db_url: str, + retry_count: int = 5, + get_db_url: Optional[Callable[[], Optional[str]]] = None, + unbind: Optional[Callable[[], None]] = None, + ) -> None: """Initialize database. You shouldn't have to do this manually. Args: db_url (str): Database url to use. retry_count (int): How many times to retry connecting (with exponential backoff) + get_db_url (callable[[], str]): A function that will be called to refresh + the db_url property + unbind (callable[[], None]): A callback to clean up after .close() is called """ self.db_url = db_url self.sess = aiohttp.ClientSession() + self._get_db_url = get_db_url + self._unbind = unbind retry_options = ExponentialRetry(attempts=retry_count) self.client = RetryClient(client_session=self.sess, retry_options=retry_options) + if self._get_db_url: + self._refresh_timer = threading.Timer(3600, self._refresh_db) + self._refresh_timer.start() + + def _refresh_db(self) -> None: + if self._refresh_timer: + self._refresh_timer.cancel() + self._refresh_timer = None + if self._get_db_url: + db_url = self._get_db_url() + if db_url: + self.update_db_url(db_url) + self._refresh_timer = threading.Timer(3600, self._refresh_db) + self._refresh_timer.start() + def update_db_url(self, db_url: str) -> None: """Update the database url. @@ -239,6 +273,16 @@ async def items(self) -> Tuple[Tuple[str, str], ...]: """ return tuple((await self.to_dict()).items()) + async def close(self) -> None: + """Closes the database client connection.""" + await self.sess.close() + if self._refresh_timer: + self._refresh_timer.cancel() + self._refresh_timer = None + if self._unbind: + # Permit signaling to surrounding scopes that we have closed + self._unbind() + def __repr__(self) -> str: """A representation of the database. @@ -417,30 +461,62 @@ def item_to_observed(on_mutate: Callable[[Any], None], item: Any) -> Any: class Database(abc.MutableMapping): - """Dictionary-like interface for Repl.it Database. + """Dictionary-like interface for Replit Database. This interface will coerce all values everything to and from JSON. If you don't want this, use AsyncDatabase instead. + + :param str db_url: The Database URL to connect to + :param int retry_count: How many retry attempts we should make + :param get_db_url Callable: A callback that returns the current db_url + :param unbind Callable: Permit additional behavior after Database close """ - __slots__ = ("db_url", "sess") + __slots__ = ("db_url", "sess", "_get_db_url", "_unbind", "_refresh_timer") + _refresh_timer: Optional[threading.Timer] - def __init__(self, db_url: str, retry_count: int = 5) -> None: + def __init__( + self, + db_url: str, + retry_count: int = 5, + get_db_url: Optional[Callable[[], Optional[str]]] = None, + unbind: Optional[Callable[[], None]] = None, + ) -> None: """Initialize database. You shouldn't have to do this manually. Args: db_url (str): Database url to use. retry_count (int): How many times to retry connecting (with exponential backoff) + get_db_url (callable[[], str]): A function that will be called to refresh + the db_url property + unbind (callable[[], None]): A callback to clean up after .close() is called """ self.db_url = db_url self.sess = requests.Session() + self._get_db_url = get_db_url + self._unbind = unbind retries = Retry( total=retry_count, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504] ) self.sess.mount("http://", HTTPAdapter(max_retries=retries)) self.sess.mount("https://", HTTPAdapter(max_retries=retries)) + if self._get_db_url: + self._refresh_timer = threading.Timer(3600, self._refresh_db) + self._refresh_timer.start() + + def _refresh_db(self) -> None: + if self._refresh_timer: + self._refresh_timer.cancel() + self._refresh_timer = None + if self._get_db_url: + db_url = self._get_db_url() + if db_url: + self.update_db_url(db_url) + self._refresh_timer = threading.Timer(3600, self._refresh_db) + self._refresh_timer.start() + def update_db_url(self, db_url: str) -> None: """Update the database url. @@ -627,3 +703,9 @@ def __repr__(self) -> str: def close(self) -> None: """Closes the database client connection.""" self.sess.close() + if self._refresh_timer: + self._refresh_timer.cancel() + self._refresh_timer = None + if self._unbind: + # Permit signaling to surrounding scopes that we have closed + self._unbind() diff --git a/src/replit/database/default_db.py b/src/replit/database/default_db.py index 5e080e6d..7b6e1427 100644 --- a/src/replit/database/default_db.py +++ b/src/replit/database/default_db.py @@ -1,8 +1,6 @@ """A module containing the default database.""" -import logging import os import os.path -import threading from typing import Any, Optional from .database import Database @@ -20,51 +18,32 @@ def get_db_url() -> Optional[str]: def refresh_db() -> None: - """Deprecated: refresh_db is now located inside the LazyDB singleton instance.""" + """Deprecated: refresh_db is now the responsibility of the Database instance.""" pass -class LazyDB: - """A way to lazily create a database connection.""" - - _instance: Optional["LazyDB"] = 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." - ) - - 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": - """Get the lazy singleton instance.""" - if cls._instance is None: - cls._instance = LazyDB() - return cls._instance - - @classmethod - def get_db(cls) -> Optional[Database]: - """Get a reference to the singleton Database instance.""" - return cls.get_instance()._db - - @classmethod - def get_db_url(cls) -> Optional[str]: - """Get the db_url connection string.""" - return cls.get_instance()._db_url +def _unbind() -> None: + global _db + _db = None + + +def _get_db() -> Optional[Database]: + global _db + if _db is not None: + return _db + + db_url = get_db_url() + + if db_url: + _db = Database(db_url, get_db_url=get_db_url, unbind=_unbind) + 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 + return _db + + +_db: Optional[Database] = None # Previous versions of this library would just have side-effects and always set @@ -73,7 +52,7 @@ def get_db_url(cls) -> Optional[str]: # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_db() + return _get_db() if name == "db_url": - return LazyDB.get_db_url() + return get_db_url() raise AttributeError(name) diff --git a/src/replit/database/server.py b/src/replit/database/server.py index 237f7d3c..6b70b30d 100644 --- a/src/replit/database/server.py +++ b/src/replit/database/server.py @@ -4,7 +4,7 @@ from flask import Blueprint, Flask, request -from .default_db import LazyDB +from . import default_db def make_database_proxy_blueprint(view_only: bool, prefix: str = "") -> Blueprint: @@ -20,12 +20,11 @@ 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: - db = LazyDB.get_db() - if db is None: + if default_db.db is None: return "Database is not configured", 500 user_prefix = request.args.get("prefix", "") encode = "encode" in request.args - raw_keys = db.prefix(prefix=prefix + user_prefix) + raw_keys = default_db.db.prefix(prefix=prefix + user_prefix) keys = [k[len(prefix) :] for k in raw_keys] if encode: @@ -34,13 +33,12 @@ def list_keys() -> Any: return "\n".join(keys) def set_key() -> Any: - db = LazyDB.get_db() - if db is None: + if default_db.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(): - db[prefix + k] = v + default_db.db[prefix + k] = v return "" @app.route("/", methods=["GET", "POST"]) @@ -50,22 +48,20 @@ def index() -> Any: return set_key() def get_key(key: str) -> Any: - db = LazyDB.get_db() - if db is None: + if default_db.db is None: return "Database is not configured", 500 try: - return db[prefix + key] + return default_db.db[prefix + key] except KeyError: return "", 404 def delete_key(key: str) -> Any: - db = LazyDB.get_db() - if db is None: + if default_db.db is None: return "Database is not configured", 500 if view_only: return "Database is view only", 401 try: - del db[prefix + key] + del default_db.db[prefix + key] except KeyError: return "", 404 return "" diff --git a/src/replit/web/__init__.py b/src/replit/web/__init__.py index ec8e6ba2..03c86f1b 100644 --- a/src/replit/web/__init__.py +++ b/src/replit/web/__init__.py @@ -9,7 +9,8 @@ from .app import debug, ReplitAuthContext, run from .user import User, UserStore from .utils import * -from ..database import AsyncDatabase, Database, LazyDB +from .. import database +from ..database import AsyncDatabase, Database auth = LocalProxy(lambda: ReplitAuthContext.from_headers(flask.request.headers)) @@ -20,5 +21,5 @@ # lazily. def __getattr__(name: str) -> Any: if name == "db": - return LazyDB.get_db() + return database.db raise AttributeError(name) diff --git a/src/replit/web/user.py b/src/replit/web/user.py index cb635369..133e6c37 100644 --- a/src/replit/web/user.py +++ b/src/replit/web/user.py @@ -5,7 +5,7 @@ import flask from .app import ReplitAuthContext -from ..database import LazyDB +from .. import database class User(MutableMapping): @@ -33,20 +33,18 @@ def set_value(self, value: str) -> None: Raises: RuntimeError: Raised if the database is not configured. """ - db = LazyDB.get_db() - if db is None: + if database.db is None: raise RuntimeError("database not configured") - db[self.db_key()] = value + database.db[self.db_key()] = value def _ensure_value(self) -> Any: - db = LazyDB.get_db() - if db is None: + if database.db is None: raise RuntimeError("database not configured") try: - return db[self.db_key()] + return database.db[self.db_key()] except KeyError: - db[self.db_key()] = {} - return db[self.db_key()] + database.db[self.db_key()] = {} + return database.db[self.db_key()] def set(self, key: str, val: Any) -> None: """Sets a key to a value for this user's entry in the database. @@ -110,10 +108,9 @@ def __getitem__(self, name: str) -> User: return User(username=name, prefix=self.prefix) def __iter__(self) -> Iterator[str]: - db = LazyDB.get_db() - if db is None: + if database.db is None: raise RuntimeError("database not configured") - for k in db.keys(): + for k in database.db.keys(): if k.startswith(self.prefix): yield self._strip_prefix(k) From e4d5a025c262ef39c77cf967afabf421923b2b4a Mon Sep 17 00:00:00 2001 From: Devon Stewart Date: Mon, 26 Feb 2024 22:07:51 +0000 Subject: [PATCH 5/5] Removing stale main.sh --- main.sh | 1 - 1 file changed, 1 deletion(-) delete mode 100644 main.sh diff --git a/main.sh b/main.sh deleted file mode 100644 index 975cd92a..00000000 --- a/main.sh +++ /dev/null @@ -1 +0,0 @@ -python testapp.py \ No newline at end of file