# retoor <retoor@molodetz.nl>
import inspect
import os
import httpx
from devplacepy.cache import TTLCache
from devplacepy_services.base.db_codec import (
decode_value,
encode_args,
is_write,
is_write_sql,
)
_SERVICE_URL = os.environ.get("DEVPLACE_DB_SERVICE_URL", "http://127.0.0.1:10601").rstrip("/")
_INTERNAL_KEY = os.environ.get("DEVPLACE_GATEWAY_INTERNAL_KEY", "").strip()
_CLIENT: httpx.Client | None = None
# Section 7.3: settings reads tolerate up to 5s staleness. patch_module()
# generically RPCs every devplacepy.database call, bypassing the local
# TTL cache get_setting/get_int_setting had in-process - without this,
# every settings read (rate limiting, maintenance mode, admin dashboards)
# pays a full HTTP round trip to the database broker.
_SETTINGS_CACHE_TTL_SECONDS = 5
_SETTINGS_CACHE = TTLCache(ttl=_SETTINGS_CACHE_TTL_SECONDS, max_size=512)
_CACHED_SETTINGS_FNS = frozenset({"get_setting", "get_int_setting"})
def _service_url() -> str:
return os.environ.get("DEVPLACE_DB_SERVICE_URL", _SERVICE_URL).rstrip("/")
def _headers() -> dict[str, str]:
headers: dict[str, str] = {}
key = os.environ.get("DEVPLACE_GATEWAY_INTERNAL_KEY", _INTERNAL_KEY).strip()
if key:
headers["X-Internal-Key"] = key
return headers
def _client() -> httpx.Client:
global _CLIENT
if _CLIENT is None:
_CLIENT = httpx.Client(timeout=30.0)
return _CLIENT
def _post(path: str, body: dict) -> object:
response = _client().post(
f"{_service_url()}/{path.lstrip('/')}",
json=body,
headers=_headers(),
)
if response.status_code >= 400:
payload = response.json() if response.content else {}
message = payload.get("error", "Database service request failed")
raise RuntimeError(message)
if not response.content:
return None
return decode_value(response.json())
def _invoke_cached(fn_name: str, args, kwargs):
cache_key = f"{fn_name}:{args!r}:{sorted(kwargs.items())!r}"
cached = _SETTINGS_CACHE.get(cache_key)
if cached is not None:
return cached
value = _invoke(fn_name, args, kwargs, write=False)
_SETTINGS_CACHE.set(cache_key, value)
return value
def _invoke(fn_name: str, args, kwargs, *, write: bool = False):
encoded_args, encoded_kwargs = encode_args(args, kwargs)
payload = {
"fn": fn_name,
"args": encoded_args,
"kwargs": encoded_kwargs,
"write": write,
}
result = _post("internal/invoke", payload)
if isinstance(result, dict) and "result" in result:
return result["result"]
return result
class RemoteSearchClause:
def __init__(self, term, fields, author_field=None):
self.term = term.strip()
self.fields = tuple(fields)
self.author_field = author_field
class RemoteUidInClause:
def __init__(self, field, uids):
self.field = field
self.uids = frozenset(uids)
class RemoteTable:
def __init__(self, db: "RemoteDb", name: str) -> None:
self._db = db
self._name = name
self._column_cache = None
def __getattr__(self, name: str):
def caller(*args, **kwargs):
return self._db._table_op(self._name, name, args, kwargs)
return caller
def has_column(self, name: str) -> bool:
cache = self._column_cache
if cache is None:
sample = self.find(_limit=1)
row = next(iter(sample), None)
cache = set(row.keys()) if row else set()
self._column_cache = cache
return name in cache
def count(self, **kwargs):
return self._db._table_op(self._name, "count", [], kwargs)
@property
def table(self):
return self
@property
def exists(self) -> bool:
return self._name in self._db.tables
class RemoteDb:
def __init__(self) -> None:
self._tables_cache: list[str] | None = None
@property
def tables(self) -> list[str]:
if self._tables_cache is None:
result = _post("internal/db-op", {"op": "tables"})
self._tables_cache = list(result or [])
return self._tables_cache
def __getitem__(self, name: str) -> RemoteTable:
return RemoteTable(self, name)
def query(self, sql: str, **params):
encoded_args, encoded_kwargs = encode_args((sql,), params)
result = _post(
"internal/db-op",
{
"op": "query",
"args": encoded_args,
"kwargs": encoded_kwargs,
"write": is_write_sql(sql),
},
)
return result or []
def _table_op(self, table: str, method: str, args, kwargs, *, write: bool = False):
encoded_args, encoded_kwargs = encode_args(args, kwargs)
result = _post(
"internal/db-op",
{
"op": "table_op",
"table": table,
"method": method,
"args": encoded_args,
"kwargs": encoded_kwargs,
"write": write,
},
)
if method in {"insert", "update", "delete"}:
self._tables_cache = None
return result
@property
def executable(self):
return self
@property
def in_transaction(self) -> bool:
return False
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
_LOCAL_REMOTE = frozenset(
{
"get_table",
"refresh_snapshot",
"_in_clause",
"_now_iso",
"text_search_clause",
}
)
def _remote_text_search_clause(
table, search, fields=("title", "description"), author_field=None
):
term = (search or "").strip()
if not term:
return None
if type(table).__name__ == "RemoteTable":
return RemoteSearchClause(term, fields, author_field)
from devplacepy.database.content import text_search_clause as local_clause
return local_clause(table, search, fields, author_field=author_field)
def _remote_get_table(name: str):
import devplacepy.database.core as core
return core.db[name]
def _remote_refresh_snapshot() -> None:
return None
def patch_module(module) -> None:
import devplacepy.database as db_module
for name in db_module.__all__:
if name in _LOCAL_REMOTE:
continue
target = getattr(module, name, None)
if target is None or not callable(target):
continue
if inspect.isclass(target):
continue
def make_wrapper(fn_name: str, fn_write: bool):
if fn_name in _CACHED_SETTINGS_FNS:
def wrapper(*args, **kwargs):
return _invoke_cached(fn_name, args, kwargs)
wrapper.__name__ = fn_name
return wrapper
def wrapper(*args, **kwargs):
return _invoke(fn_name, args, kwargs, write=fn_write)
wrapper.__name__ = fn_name
return wrapper
setattr(module, name, make_wrapper(name, is_write(name)))
def activate() -> None:
import devplacepy.database.core as core
core.db = RemoteDb()
import devplacepy.database as db_module
patch_module(db_module)
for submodule_name in (
"settings",
"users",
"relations",
"pagination",
"soft_delete",
"engagement",
"usage",
"awards",
"seo_meta",
"activity",
"customization",
"email",
"notifications",
"forks",
"follows",
"deepsearch",
"ranking",
"comments",
"content",
"attachments_data",
"stats",
"schema",
):
try:
submodule = __import__(
f"devplacepy.database.{submodule_name}",
fromlist=[submodule_name],
)
except ImportError:
continue
patch_module(submodule)
for external_name in (
"devplacepy.services.statistics.tracking",
"devplacepy.services.base",
"devplacepy.attachments",
"devplacepy.project_files",
):
try:
external = __import__(external_name, fromlist=[external_name.split(".")[-1]])
except ImportError:
continue
if hasattr(external, "db"):
external.db = RemoteDb()
db_module.db = core.db
db_module.get_table = _remote_get_table
core.get_table = _remote_get_table
db_module.refresh_snapshot = _remote_refresh_snapshot
core.refresh_snapshot = _remote_refresh_snapshot
db_module.text_search_clause = _remote_text_search_clause
import devplacepy.database.content as content_module
content_module.text_search_clause = _remote_text_search_clause
for submodule_name in (
"settings",
"users",
"relations",
"pagination",
"soft_delete",
"engagement",
"usage",
"awards",
"seo_meta",
"activity",
"customization",
"email",
"notifications",
"forks",
"follows",
"deepsearch",
"ranking",
"comments",
"content",
"attachments_data",
"stats",
"schema",
):
try:
submodule = __import__(
f"devplacepy.database.{submodule_name}",
fromlist=[submodule_name],
)
except ImportError:
continue
if hasattr(submodule, "db"):
submodule.db = core.db