Added DbObjects
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ app.egg-info
|
||||
*.pyc
|
||||
.mypy_cache
|
||||
.coverage
|
||||
.myFastHtmlDb
|
||||
htmlcov
|
||||
.cache
|
||||
.venv
|
||||
|
||||
@@ -36,6 +36,7 @@ markdown-it-py==4.0.0
|
||||
mdurl==0.1.2
|
||||
more-itertools==10.8.0
|
||||
myauth==0.2.0
|
||||
mydbengine==0.1.0
|
||||
myutils==0.4.0
|
||||
nh3==0.3.1
|
||||
oauthlib==3.3.1
|
||||
@@ -64,11 +65,13 @@ rfc3986==2.0.0
|
||||
rich==14.2.0
|
||||
rsa==4.9.1
|
||||
SecretStorage==3.4.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
soupsieve==2.8
|
||||
starlette==0.48.0
|
||||
twine==6.2.0
|
||||
typer==0.20.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.5.0
|
||||
|
||||
@@ -4,6 +4,8 @@ from fasthtml import serve
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.Layout import Layout
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.instances import InstancesManager
|
||||
from myfasthtml.myfastapp import create_app
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -22,7 +24,7 @@ app, rt = create_app(protect_routes=True,
|
||||
|
||||
@rt("/")
|
||||
def index(session):
|
||||
layout = Layout(session, "Testing Layout")
|
||||
layout = InstancesManager.get(session, Ids.Layout, Layout, "Testing Layout")
|
||||
layout.set_footer("Goodbye World")
|
||||
for i in range(1000):
|
||||
layout.left_drawer.append(Div(f"Left Drawer Item {i}"))
|
||||
|
||||
@@ -13,7 +13,8 @@ from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.UserProfile import UserProfile
|
||||
from myfasthtml.controls.helpers import mk, Ids
|
||||
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_p2 import panel_right_expand20_regular as right_drawer_icon
|
||||
|
||||
@@ -21,7 +22,10 @@ logger = logging.getLogger("LayoutControl")
|
||||
|
||||
|
||||
@dataclass
|
||||
class LayoutState:
|
||||
class LayoutState(DbObject):
|
||||
def __init__(self, session, owner):
|
||||
super().__init__(session, owner.get_id())
|
||||
|
||||
left_drawer_open: bool = True
|
||||
right_drawer_open: bool = False
|
||||
|
||||
@@ -31,7 +35,7 @@ class Commands(BaseCommands):
|
||||
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,
|
||||
and optional collapsible side drawers.
|
||||
@@ -70,7 +74,7 @@ class Layout(MultipleInstance):
|
||||
self._header_content = None
|
||||
self._footer_content = None
|
||||
self._main_content = None
|
||||
self._state = LayoutState()
|
||||
self._state = LayoutState(session, self)
|
||||
self.commands = Commands(self)
|
||||
self.left_drawer = self.DrawerContent(self, "left")
|
||||
self.right_drawer = self.DrawerContent(self, "right")
|
||||
|
||||
@@ -2,7 +2,7 @@ from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.helpers import Ids, mk
|
||||
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_p1 import light_mode_filled, alternate_email_filled
|
||||
|
||||
@@ -12,7 +12,7 @@ class UserProfile(SingleInstance):
|
||||
super().__init__(session, Ids.UserProfile)
|
||||
|
||||
def render(self):
|
||||
user_info = get_user_info(self._session)
|
||||
user_info = retrieve_user_info(self._session)
|
||||
return Div(
|
||||
Div(user_info['username'],
|
||||
tabindex="0",
|
||||
|
||||
@@ -3,10 +3,14 @@ from fasthtml.components import *
|
||||
from myfasthtml.core.bindings import Binding
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.utils import merge_classes
|
||||
|
||||
|
||||
class Ids:
|
||||
DbManager = "mf-dbmanager"
|
||||
Layout = "mf-layout"
|
||||
UserProfile = "mf-user-profile"
|
||||
|
||||
|
||||
class mk:
|
||||
|
||||
@staticmethod
|
||||
|
||||
62
src/myfasthtml/core/dbmanager.py
Normal file
62
src/myfasthtml/core/dbmanager.py
Normal 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)
|
||||
@@ -11,9 +11,10 @@ class BaseInstance:
|
||||
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._id = _id
|
||||
if auto_register:
|
||||
InstancesManager.register(session, self)
|
||||
|
||||
def get_id(self):
|
||||
@@ -28,8 +29,8 @@ class SingleInstance(BaseInstance):
|
||||
Base class for instances that can only have one instance at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, session: dict, prefix: str):
|
||||
super().__init__(session, prefix)
|
||||
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
||||
super().__init__(session, prefix, auto_register)
|
||||
self._instance = None
|
||||
|
||||
|
||||
@@ -38,8 +39,8 @@ class MultipleInstance(BaseInstance):
|
||||
Base class for instances that can have multiple instances at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, session: dict, prefix: str):
|
||||
super().__init__(session, f"{prefix}-{str(uuid.uuid4())}")
|
||||
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
||||
super().__init__(session, f"{prefix}-{str(uuid.uuid4())}", auto_register)
|
||||
self._instance = None
|
||||
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ def quoted_str(s):
|
||||
return str(s)
|
||||
|
||||
|
||||
def get_user_info(session: dict):
|
||||
def retrieve_user_info(session: dict):
|
||||
if not session:
|
||||
return {
|
||||
"id": "** NOT LOGGED IN **",
|
||||
|
||||
98
tests/core/test_db_object.py
Normal file
98
tests/core/test_db_object.py
Normal 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__"]
|
||||
Reference in New Issue
Block a user