Added DbObjects
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ app.egg-info
|
|||||||
*.pyc
|
*.pyc
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.coverage
|
.coverage
|
||||||
|
.myFastHtmlDb
|
||||||
htmlcov
|
htmlcov
|
||||||
.cache
|
.cache
|
||||||
.venv
|
.venv
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 **",
|
||||||
|
|||||||
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