Added DbObjects

This commit is contained in:
2025-11-10 23:12:07 +01:00
parent d302261d07
commit c641f3fd63
10 changed files with 189 additions and 14 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ app.egg-info
*.pyc *.pyc
.mypy_cache .mypy_cache
.coverage .coverage
.myFastHtmlDb
htmlcov htmlcov
.cache .cache
.venv .venv

View File

@@ -36,6 +36,7 @@ markdown-it-py==4.0.0
mdurl==0.1.2 mdurl==0.1.2
more-itertools==10.8.0 more-itertools==10.8.0
myauth==0.2.0 myauth==0.2.0
mydbengine==0.1.0
myutils==0.4.0 myutils==0.4.0
nh3==0.3.1 nh3==0.3.1
oauthlib==3.3.1 oauthlib==3.3.1
@@ -64,11 +65,13 @@ rfc3986==2.0.0
rich==14.2.0 rich==14.2.0
rsa==4.9.1 rsa==4.9.1
SecretStorage==3.4.0 SecretStorage==3.4.0
shellingham==1.5.4
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
soupsieve==2.8 soupsieve==2.8
starlette==0.48.0 starlette==0.48.0
twine==6.2.0 twine==6.2.0
typer==0.20.0
typing-inspection==0.4.2 typing-inspection==0.4.2
typing_extensions==4.15.0 typing_extensions==4.15.0
urllib3==2.5.0 urllib3==2.5.0

View File

@@ -4,6 +4,8 @@ from fasthtml import serve
from fasthtml.components import * from fasthtml.components import *
from myfasthtml.controls.Layout import Layout from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import Ids
from myfasthtml.core.instances import InstancesManager
from myfasthtml.myfastapp import create_app from myfasthtml.myfastapp import create_app
logging.basicConfig( logging.basicConfig(
@@ -22,7 +24,7 @@ app, rt = create_app(protect_routes=True,
@rt("/") @rt("/")
def index(session): def index(session):
layout = Layout(session, "Testing Layout") layout = InstancesManager.get(session, Ids.Layout, Layout, "Testing Layout")
layout.set_footer("Goodbye World") layout.set_footer("Goodbye World")
for i in range(1000): for i in range(1000):
layout.left_drawer.append(Div(f"Left Drawer Item {i}")) layout.left_drawer.append(Div(f"Left Drawer Item {i}"))

View File

@@ -13,7 +13,8 @@ from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.UserProfile import UserProfile from myfasthtml.controls.UserProfile import UserProfile
from myfasthtml.controls.helpers import mk, Ids from myfasthtml.controls.helpers import mk, Ids
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.instances import MultipleInstance, InstancesManager from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import InstancesManager, SingleInstance
from myfasthtml.icons.fluent import panel_left_expand20_regular as left_drawer_icon from myfasthtml.icons.fluent import panel_left_expand20_regular as left_drawer_icon
from myfasthtml.icons.fluent_p2 import panel_right_expand20_regular as right_drawer_icon from myfasthtml.icons.fluent_p2 import panel_right_expand20_regular as right_drawer_icon
@@ -21,7 +22,10 @@ logger = logging.getLogger("LayoutControl")
@dataclass @dataclass
class LayoutState: class LayoutState(DbObject):
def __init__(self, session, owner):
super().__init__(session, owner.get_id())
left_drawer_open: bool = True left_drawer_open: bool = True
right_drawer_open: bool = False right_drawer_open: bool = False
@@ -31,7 +35,7 @@ class Commands(BaseCommands):
return Command("ToggleDrawer", "Toggle main layout drawer", self._owner.toggle_drawer, "left") return Command("ToggleDrawer", "Toggle main layout drawer", self._owner.toggle_drawer, "left")
class Layout(MultipleInstance): class Layout(SingleInstance):
""" """
A responsive layout component with header, footer, main content area, A responsive layout component with header, footer, main content area,
and optional collapsible side drawers. and optional collapsible side drawers.
@@ -70,7 +74,7 @@ class Layout(MultipleInstance):
self._header_content = None self._header_content = None
self._footer_content = None self._footer_content = None
self._main_content = None self._main_content = None
self._state = LayoutState() self._state = LayoutState(session, self)
self.commands = Commands(self) self.commands = Commands(self)
self.left_drawer = self.DrawerContent(self, "left") self.left_drawer = self.DrawerContent(self, "left")
self.right_drawer = self.DrawerContent(self, "right") self.right_drawer = self.DrawerContent(self, "right")

View File

@@ -2,7 +2,7 @@ from fasthtml.components import *
from myfasthtml.controls.helpers import Ids, mk from myfasthtml.controls.helpers import Ids, mk
from myfasthtml.core.instances import SingleInstance from myfasthtml.core.instances import SingleInstance
from myfasthtml.core.utils import get_user_info from myfasthtml.core.utils import retrieve_user_info
from myfasthtml.icons.material import dark_mode_filled, person_outline_sharp from myfasthtml.icons.material import dark_mode_filled, person_outline_sharp
from myfasthtml.icons.material_p1 import light_mode_filled, alternate_email_filled from myfasthtml.icons.material_p1 import light_mode_filled, alternate_email_filled
@@ -12,7 +12,7 @@ class UserProfile(SingleInstance):
super().__init__(session, Ids.UserProfile) super().__init__(session, Ids.UserProfile)
def render(self): def render(self):
user_info = get_user_info(self._session) user_info = retrieve_user_info(self._session)
return Div( return Div(
Div(user_info['username'], Div(user_info['username'],
tabindex="0", tabindex="0",

View File

@@ -3,10 +3,14 @@ from fasthtml.components import *
from myfasthtml.core.bindings import Binding from myfasthtml.core.bindings import Binding
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.utils import merge_classes from myfasthtml.core.utils import merge_classes
class Ids: class Ids:
DbManager = "mf-dbmanager"
Layout = "mf-layout" Layout = "mf-layout"
UserProfile = "mf-user-profile" UserProfile = "mf-user-profile"
class mk: class mk:
@staticmethod @staticmethod

View File

@@ -0,0 +1,62 @@
from dbengine.dbengine import DbEngine
from myfasthtml.controls.helpers import Ids
from myfasthtml.core.instances import SingleInstance, InstancesManager
from myfasthtml.core.utils import retrieve_user_info
class DbManager(SingleInstance):
def __init__(self, session, root=".myFastHtmlDb", auto_register: bool = True):
super().__init__(session, Ids.DbManager, auto_register=auto_register)
self.db = DbEngine(root=root)
def save(self, entry, obj):
self.db.save(self.get_tenant(), self.get_user(), entry, obj)
def load(self, entry):
return self.db.load(self.get_tenant(), entry)
def exists_entry(self, entry):
return self.db.exists(self.get_tenant(), entry)
def get_tenant(self):
return retrieve_user_info(self._session)["id"]
def get_user(self):
return retrieve_user_info(self._session)["email"]
class DbObject:
"""
When you set the attribute, it persists in DB
It loads from DB at startup
"""
def __init__(self, session, name=None, db_manager=None):
self._session = session
self._name = name or self.__class__.__name__
self._db_manager = db_manager or InstancesManager.get(self._session, Ids.DbManager, DbManager)
# init is possible
if self._db_manager.exists_entry(self._name):
props = self._db_manager.load(self._name)
for k, v in props.items():
setattr(self, k, v)
else:
self._save_self()
def __setattr__(self, name: str, value: str):
if name.startswith("_"):
super().__setattr__(name, value)
return
old_value = getattr(self, name, None)
if old_value == value:
return
super().__setattr__(name, value)
self._save_self()
def _save_self(self):
props = {k: getattr(self, k) for k, v in self.__class__.__dict__.items() if not k.startswith("_")}
self._db_manager.save(self._name, props)

View File

@@ -11,10 +11,11 @@ class BaseInstance:
Base class for all instances (manageable by InstancesManager) Base class for all instances (manageable by InstancesManager)
""" """
def __init__(self, session: dict, _id: str): def __init__(self, session: dict, _id: str, auto_register: bool = True):
self._session = session self._session = session
self._id = _id self._id = _id
InstancesManager.register(session, self) if auto_register:
InstancesManager.register(session, self)
def get_id(self): def get_id(self):
return self._id return self._id
@@ -28,8 +29,8 @@ class SingleInstance(BaseInstance):
Base class for instances that can only have one instance at a time. Base class for instances that can only have one instance at a time.
""" """
def __init__(self, session: dict, prefix: str): def __init__(self, session: dict, prefix: str, auto_register: bool = True):
super().__init__(session, prefix) super().__init__(session, prefix, auto_register)
self._instance = None self._instance = None
@@ -38,8 +39,8 @@ class MultipleInstance(BaseInstance):
Base class for instances that can have multiple instances at a time. Base class for instances that can have multiple instances at a time.
""" """
def __init__(self, session: dict, prefix: str): def __init__(self, session: dict, prefix: str, auto_register: bool = True):
super().__init__(session, f"{prefix}-{str(uuid.uuid4())}") super().__init__(session, f"{prefix}-{str(uuid.uuid4())}", auto_register)
self._instance = None self._instance = None

View File

@@ -191,7 +191,7 @@ def quoted_str(s):
return str(s) return str(s)
def get_user_info(session: dict): def retrieve_user_info(session: dict):
if not session: if not session:
return { return {
"id": "** NOT LOGGED IN **", "id": "** NOT LOGGED IN **",

View File

@@ -0,0 +1,98 @@
import shutil
from dataclasses import dataclass
import pytest
from myfasthtml.core.dbmanager import DbManager, DbObject
@pytest.fixture(scope="session")
def session():
return {
"user_info": {
"id": "test_tenant_id",
"email": "test@email.com",
"username": "test user",
"role": [],
}
}
@pytest.fixture
def db_manager(session):
shutil.rmtree("TestDb", ignore_errors=True)
db_manager_instance = DbManager(session, root="TestDb", auto_register=False)
yield db_manager_instance
shutil.rmtree("TestDb", ignore_errors=True)
def simplify(res: dict) -> dict:
return {k: v for k, v in res.items() if not k.startswith("_")}
def test_i_can_init(session, db_manager):
@dataclass
class DummyObject(DbObject):
def __init__(self, sess: dict):
super().__init__(sess, "DummyObject", db_manager)
value: str = "hello"
number: int = 42
DummyObject(session)
assert simplify(db_manager.load("DummyObject")) == {"value": "hello", "number": 42}
def test_i_can_init_from_db(session, db_manager):
@dataclass
class DummyObject(DbObject):
def __init__(self, sess: dict):
super().__init__(sess, "DummyObject", db_manager)
value: str = "hello"
number: int = 42
# insert other values in db
db_manager.save("DummyObject", {"value": "other_value", "number": 34})
dummy = DummyObject(session)
assert dummy.value == "other_value"
assert dummy.number == 34
def test_db_is_updated_when_attribute_is_modified(session, db_manager):
@dataclass
class DummyObject(DbObject):
def __init__(self, sess: dict):
super().__init__(sess, "DummyObject", db_manager)
value: str = "hello"
number: int = 42
dummy = DummyObject(session)
dummy.value = "other_value"
assert simplify(db_manager.load("DummyObject")) == {"value": "other_value", "number": 42}
def test_i_do_not_save_in_db_when_value_is_the_same(session, db_manager):
@dataclass
class DummyObject(DbObject):
def __init__(self, sess: dict):
super().__init__(sess, "DummyObject", db_manager)
value: str = "hello"
number: int = 42
dummy = DummyObject(session)
dummy.value = "other_value"
in_db_1 = db_manager.load("DummyObject")
dummy.value = "other_value"
in_db_2 = db_manager.load("DummyObject")
assert in_db_1["__parent__"] == in_db_2["__parent__"]