Refactored instances management
This commit is contained in:
@@ -26,8 +26,8 @@ class Boundaries(SingleInstance):
|
||||
Keep the boundaries updated
|
||||
"""
|
||||
|
||||
def __init__(self, session, owner, container_id: str = None, on_resize=None):
|
||||
super().__init__(session, Ids.Boundaries, owner)
|
||||
def __init__(self, owner, container_id: str = None, on_resize=None, _id=None):
|
||||
super().__init__(owner, _id=_id)
|
||||
self._owner = owner
|
||||
self._container_id = container_id or owner.get_id()
|
||||
self._on_resize = on_resize
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.commands import CommandsManager
|
||||
from myfasthtml.core.instances import SingleInstance
|
||||
from myfasthtml.core.network_utils import from_parent_child_list
|
||||
|
||||
|
||||
class CommandsDebugger(SingleInstance):
|
||||
def __init__(self, session, parent, _id=None):
|
||||
super().__init__(session, Ids.CommandsDebugger, parent)
|
||||
def __init__(self, parent, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
|
||||
def render(self):
|
||||
commands = self._get_commands()
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger("FileUpload")
|
||||
|
||||
class FileUploadState(DbObject):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner.get_session(), owner.get_id())
|
||||
super().__init__(owner)
|
||||
with self.initializing():
|
||||
# persisted in DB
|
||||
|
||||
@@ -37,7 +37,7 @@ class Commands(BaseCommands):
|
||||
class FileUpload(MultipleInstance):
|
||||
|
||||
def __init__(self, parent, _id=None):
|
||||
super().__init__(Ids.FileUpload, parent, _id=_id)
|
||||
super().__init__(parent, _id=_id)
|
||||
self.commands = Commands(self)
|
||||
self._state = FileUploadState(self)
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||
from myfasthtml.core.network_utils import from_parent_child_list
|
||||
|
||||
|
||||
class InstancesDebugger(SingleInstance):
|
||||
def __init__(self, session, parent, _id=None):
|
||||
super().__init__(session, Ids.InstancesDebugger, parent)
|
||||
def __init__(self, parent, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
|
||||
def render(self):
|
||||
instances = self._get_instances()
|
||||
@@ -15,8 +14,15 @@ class InstancesDebugger(SingleInstance):
|
||||
label_getter=lambda x: x.get_prefix(),
|
||||
parent_getter=lambda x: x.get_parent().get_id() if x.get_parent() else None
|
||||
)
|
||||
for edge in edges:
|
||||
edge["color"] = "green"
|
||||
edge["arrows"] = {"to": {"enabled": False, "type": "circle"}}
|
||||
|
||||
for node in nodes:
|
||||
node["shape"] = "box"
|
||||
|
||||
vis_network = VisNetwork(self, nodes=nodes, edges=edges)
|
||||
#vis_network.add_to_options(physics={"wind": {"x": 0, "y": 1}})
|
||||
return vis_network
|
||||
|
||||
def _get_instances(self):
|
||||
|
||||
@@ -2,14 +2,13 @@ import json
|
||||
|
||||
from fasthtml.xtend import Script
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.commands import BaseCommand
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
|
||||
|
||||
class Keyboard(MultipleInstance):
|
||||
def __init__(self, parent, _id=None, combinations=None):
|
||||
super().__init__(Ids.Keyboard, parent)
|
||||
super().__init__(parent, _id=_id)
|
||||
self.combinations = combinations or {}
|
||||
|
||||
def add(self, sequence: str, command: BaseCommand):
|
||||
|
||||
@@ -12,10 +12,10 @@ from fasthtml.common import *
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.Boundaries import Boundaries
|
||||
from myfasthtml.controls.UserProfile import UserProfile
|
||||
from myfasthtml.controls.helpers import mk, Ids
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import InstancesManager, SingleInstance
|
||||
from myfasthtml.core.instances import SingleInstance
|
||||
from myfasthtml.core.utils import get_id
|
||||
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
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger("LayoutControl")
|
||||
|
||||
class LayoutState(DbObject):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner.get_session(), owner.get_id())
|
||||
super().__init__(owner)
|
||||
with self.initializing():
|
||||
self.left_drawer_open: bool = True
|
||||
self.right_drawer_open: bool = True
|
||||
@@ -100,7 +100,7 @@ class Layout(SingleInstance):
|
||||
def get_groups(self):
|
||||
return self._groups
|
||||
|
||||
def __init__(self, session, app_name, parent=None):
|
||||
def __init__(self, parent, app_name, _id=None):
|
||||
"""
|
||||
Initialize the Layout component.
|
||||
|
||||
@@ -109,13 +109,13 @@ class Layout(SingleInstance):
|
||||
left_drawer (bool): Enable left drawer. Default is True.
|
||||
right_drawer (bool): Enable right drawer. Default is True.
|
||||
"""
|
||||
super().__init__(session, Ids.Layout, parent)
|
||||
super().__init__(parent, _id=_id)
|
||||
self.app_name = app_name
|
||||
|
||||
# Content storage
|
||||
self._main_content = None
|
||||
self._state = LayoutState(self)
|
||||
self._boundaries = Boundaries(session, self)
|
||||
self._boundaries = Boundaries(self)
|
||||
self.commands = Commands(self)
|
||||
self.left_drawer = self.Content(self)
|
||||
self.right_drawer = self.Content(self)
|
||||
@@ -193,7 +193,7 @@ class Layout(SingleInstance):
|
||||
),
|
||||
Div( # right
|
||||
*self.header_right.get_content(),
|
||||
InstancesManager.get(self._session, Ids.UserProfile, UserProfile),
|
||||
UserProfile(self),
|
||||
cls="flex gap-1"
|
||||
),
|
||||
cls="mf-layout-header"
|
||||
|
||||
@@ -35,14 +35,13 @@ class Search(MultipleInstance):
|
||||
a callable for extracting a string value from items, and a template callable for rendering
|
||||
the filtered items. It provides functionality to handle and organize item-based operations.
|
||||
|
||||
:param session: The session object to maintain state or context across operations.
|
||||
:param _id: Optional identifier for the component.
|
||||
:param items: An optional list of names for the items to be filtered.
|
||||
:param get_attr: Callable function to extract a string value from an item for filtering. Defaults to a
|
||||
function that returns the item as is.
|
||||
:param template: Callable function to render the filtered items. Defaults to a Div rendering function.
|
||||
"""
|
||||
super().__init__(Ids.Search, parent, _id=_id)
|
||||
super().__init__(parent, _id=_id)
|
||||
self.items_names = items_names or ''
|
||||
self.items = items or []
|
||||
self.filtered = self.items.copy()
|
||||
|
||||
@@ -9,7 +9,7 @@ from fasthtml.xtend import Script
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.Search import Search
|
||||
from myfasthtml.controls.VisNetwork import VisNetwork
|
||||
from myfasthtml.controls.helpers import Ids, mk
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import MultipleInstance, BaseInstance
|
||||
@@ -45,7 +45,7 @@ class Boundaries:
|
||||
|
||||
class TabsManagerState(DbObject):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner.get_session(), owner.get_id())
|
||||
super().__init__(owner)
|
||||
with self.initializing():
|
||||
# persisted in DB
|
||||
self.tabs: dict[str, Any] = {}
|
||||
@@ -78,7 +78,7 @@ class TabsManager(MultipleInstance):
|
||||
_tab_count = 0
|
||||
|
||||
def __init__(self, parent, _id=None):
|
||||
super().__init__(Ids.TabsManager, parent, _id=_id)
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = TabsManagerState(self)
|
||||
self.commands = Commands(self)
|
||||
self._boundaries = Boundaries()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.helpers import Ids, mk
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.AuthProxy import AuthProxy
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager, RootInstance
|
||||
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
|
||||
@@ -25,7 +26,7 @@ class UserProfileState:
|
||||
|
||||
def save(self):
|
||||
user_settings = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
||||
auth_proxy = InstancesManager.get_auth_proxy()
|
||||
auth_proxy = AuthProxy(RootInstance)
|
||||
auth_proxy.save_user_info(self._session["access_token"], {"user_settings": user_settings})
|
||||
|
||||
|
||||
@@ -35,8 +36,8 @@ class Commands(BaseCommands):
|
||||
|
||||
|
||||
class UserProfile(SingleInstance):
|
||||
def __init__(self, session, parent=None):
|
||||
super().__init__(session, Ids.UserProfile, parent)
|
||||
def __init__(self, parent=None, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
self._state = UserProfileState(self)
|
||||
self._commands = Commands(self)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
|
||||
from fasthtml.components import Script, Div
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
|
||||
@@ -12,7 +11,7 @@ logger = logging.getLogger("VisNetwork")
|
||||
|
||||
class VisNetworkState(DbObject):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner.get_session(), owner.get_id())
|
||||
super().__init__(owner)
|
||||
with self.initializing():
|
||||
# persisted in DB
|
||||
self.nodes: list = []
|
||||
@@ -30,7 +29,7 @@ class VisNetworkState(DbObject):
|
||||
|
||||
class VisNetwork(MultipleInstance):
|
||||
def __init__(self, parent, _id=None, nodes=None, edges=None, options=None):
|
||||
super().__init__(Ids.VisNetwork, parent, _id=_id)
|
||||
super().__init__(parent, _id=_id)
|
||||
logger.debug(f"VisNetwork created with id: {self._id}")
|
||||
|
||||
self._state = VisNetworkState(self)
|
||||
@@ -50,7 +49,13 @@ class VisNetwork(MultipleInstance):
|
||||
state.options = options
|
||||
|
||||
self._state.update(state)
|
||||
|
||||
|
||||
def add_to_options(self, **kwargs):
|
||||
logger.debug(f"add_to_options: {kwargs=}")
|
||||
new_options = self._state.options.copy() | kwargs
|
||||
self._update_state(None, None, new_options)
|
||||
return self
|
||||
|
||||
def render(self):
|
||||
|
||||
# Serialize nodes and edges to JSON
|
||||
|
||||
@@ -7,19 +7,8 @@ from myfasthtml.core.utils import merge_classes
|
||||
|
||||
class Ids:
|
||||
# Please keep the alphabetical order
|
||||
AuthProxy = "mf-auth-proxy"
|
||||
Boundaries = "mf-boundaries"
|
||||
CommandsDebugger = "mf-commands-debugger"
|
||||
DbManager = "mf-dbmanager"
|
||||
FileUpload = "mf-file-upload"
|
||||
InstancesDebugger = "mf-instances-debugger"
|
||||
Keyboard = "mf-keyboard"
|
||||
Layout = "mf-layout"
|
||||
Root = "mf-root"
|
||||
Search = "mf-search"
|
||||
TabsManager = "mf-tabs-manager"
|
||||
UserProfile = "mf-user-profile"
|
||||
VisNetwork = "mf-vis-network"
|
||||
UserSession = "mf-user_session"
|
||||
|
||||
|
||||
class mk:
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from myfasthtml.auth.utils import login_user, save_user_info, register_user
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.instances import UniqueInstance, RootInstance
|
||||
from myfasthtml.core.instances import SingleInstance
|
||||
|
||||
|
||||
class AuthProxy(UniqueInstance):
|
||||
def __init__(self, base_url: str = None):
|
||||
super().__init__(Ids.AuthProxy, RootInstance)
|
||||
class AuthProxy(SingleInstance):
|
||||
def __init__(self, parent, base_url: str = None):
|
||||
super().__init__(parent)
|
||||
self._base_url = base_url
|
||||
|
||||
def login_user(self, email: str, password: str):
|
||||
|
||||
@@ -3,14 +3,13 @@ from types import SimpleNamespace
|
||||
|
||||
from dbengine.dbengine import DbEngine
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||
from myfasthtml.core.instances import SingleInstance, BaseInstance
|
||||
from myfasthtml.core.utils import retrieve_user_info
|
||||
|
||||
|
||||
class DbManager(SingleInstance):
|
||||
def __init__(self, session, parent=None, root=".myFastHtmlDb", auto_register: bool = True):
|
||||
super().__init__(session, Ids.DbManager, parent, auto_register=auto_register)
|
||||
def __init__(self, parent, root=".myFastHtmlDb", auto_register: bool = True):
|
||||
super().__init__(parent, auto_register=auto_register)
|
||||
self.db = DbEngine(root=root)
|
||||
|
||||
def save(self, entry, obj):
|
||||
@@ -35,12 +34,12 @@ class DbObject:
|
||||
It loads from DB at startup
|
||||
"""
|
||||
_initializing = False
|
||||
_forbidden_attrs = {"_initializing", "_db_manager", "_name", "_session", "_forbidden_attrs"}
|
||||
_forbidden_attrs = {"_initializing", "_db_manager", "_name", "_owner", "_forbidden_attrs"}
|
||||
|
||||
def __init__(self, session, name=None, db_manager=None):
|
||||
self._session = session
|
||||
def __init__(self, owner: BaseInstance, name=None, db_manager=None):
|
||||
self._owner = owner
|
||||
self._name = name or self.__class__.__name__
|
||||
self._db_manager = db_manager or InstancesManager.get(self._session, Ids.DbManager, DbManager)
|
||||
self._db_manager = db_manager or DbManager(self._owner)
|
||||
|
||||
self._finalize_initialization()
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import uuid
|
||||
from typing import Self
|
||||
from typing import Optional
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.utils import pascal_to_snake
|
||||
|
||||
special_session = {
|
||||
"user_info": {"id": "** SPECIAL SESSION **"}
|
||||
@@ -18,25 +19,76 @@ class BaseInstance:
|
||||
Base class for all instances (manageable by InstancesManager)
|
||||
"""
|
||||
|
||||
def __init__(self, session: dict, prefix: str, _id: str, parent: Self, auto_register: bool = True):
|
||||
self._session = session
|
||||
self._id = _id
|
||||
self._prefix = prefix
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# Extract arguments from both positional and keyword arguments
|
||||
# Signature matches __init__: parent, session=None, _id=None, auto_register=True
|
||||
parent = args[0] if len(args) > 0 and isinstance(args[0], BaseInstance) else kwargs.get("parent", None)
|
||||
session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None)
|
||||
_id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None)
|
||||
|
||||
# Compute _id if not provided
|
||||
if _id is None:
|
||||
_id = cls.compute_id()
|
||||
|
||||
if session is None:
|
||||
if parent is not None:
|
||||
session = parent.get_session()
|
||||
else:
|
||||
raise TypeError("Either session or parent must be provided")
|
||||
|
||||
session_id = InstancesManager.get_session_id(session)
|
||||
key = (session_id, _id)
|
||||
|
||||
if key in InstancesManager.instances:
|
||||
res = InstancesManager.instances[key]
|
||||
if type(res) is not cls:
|
||||
raise TypeError(f"Instance with id {_id} already exists, but is of type {type(res)}")
|
||||
return res
|
||||
|
||||
# Otherwise create a new instance
|
||||
instance = super().__new__(cls)
|
||||
instance._is_new_instance = True # mark as fresh
|
||||
return instance
|
||||
|
||||
def __init__(self, parent: Optional['BaseInstance'],
|
||||
session: Optional[dict] = None,
|
||||
_id: Optional[str] = None,
|
||||
auto_register: bool = True):
|
||||
if not getattr(self, "_is_new_instance", False):
|
||||
# Skip __init__ if instance already existed
|
||||
return
|
||||
else:
|
||||
# make sure that it's no longer considered as a new instance
|
||||
self._is_new_instance = False
|
||||
|
||||
self._parent = parent
|
||||
self._session = session or (parent.get_session() if parent else None)
|
||||
self._id = _id or self.compute_id()
|
||||
|
||||
if auto_register:
|
||||
InstancesManager.register(session, self)
|
||||
InstancesManager.register(self._session, self)
|
||||
|
||||
def get_id(self):
|
||||
return self._id
|
||||
|
||||
def get_session(self):
|
||||
def get_session(self) -> dict:
|
||||
return self._session
|
||||
|
||||
def get_prefix(self):
|
||||
return self._prefix
|
||||
def get_id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def get_parent(self):
|
||||
def get_parent(self) -> Optional['BaseInstance']:
|
||||
return self._parent
|
||||
|
||||
@classmethod
|
||||
def get_prefix(cls):
|
||||
return f"mf-{pascal_to_snake(cls.__name__)}"
|
||||
|
||||
@classmethod
|
||||
def compute_id(cls):
|
||||
prefix = cls.get_prefix()
|
||||
if issubclass(cls, SingleInstance):
|
||||
_id = prefix
|
||||
else:
|
||||
_id = f"{prefix}-{str(uuid.uuid4())}"
|
||||
return _id
|
||||
|
||||
|
||||
class SingleInstance(BaseInstance):
|
||||
@@ -44,19 +96,12 @@ class SingleInstance(BaseInstance):
|
||||
Base class for instances that can only have one instance at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, session: dict, prefix: str, parent, auto_register: bool = True):
|
||||
super().__init__(session, prefix, prefix, parent, auto_register)
|
||||
|
||||
|
||||
class UniqueInstance(BaseInstance):
|
||||
"""
|
||||
Base class for instances that can only have one instance at a time.
|
||||
Does not throw exception if the instance already exists, it simply overwrites it.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix: str, parent: BaseInstance, auto_register: bool = True):
|
||||
super().__init__(parent.get_session(), prefix, prefix, parent, auto_register)
|
||||
self._prefix = prefix
|
||||
def __init__(self,
|
||||
parent: Optional[BaseInstance] = None,
|
||||
session: Optional[dict] = None,
|
||||
_id: Optional[str] = None,
|
||||
auto_register: bool = True):
|
||||
super().__init__(parent, session, _id, auto_register)
|
||||
|
||||
|
||||
class MultipleInstance(BaseInstance):
|
||||
@@ -64,9 +109,11 @@ class MultipleInstance(BaseInstance):
|
||||
Base class for instances that can have multiple instances at a time.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix: str, parent: BaseInstance, auto_register: bool = True, _id=None):
|
||||
super().__init__(parent.get_session(), prefix, _id or f"{prefix}-{str(uuid.uuid4())}", parent, auto_register)
|
||||
self._prefix = prefix
|
||||
def __init__(self, parent: BaseInstance,
|
||||
session: Optional[dict] = None,
|
||||
_id: Optional[str] = None,
|
||||
auto_register: bool = True):
|
||||
super().__init__(parent, session, _id, auto_register)
|
||||
|
||||
|
||||
class InstancesManager:
|
||||
@@ -80,7 +127,7 @@ class InstancesManager:
|
||||
:param instance:
|
||||
:return:
|
||||
"""
|
||||
key = (InstancesManager._get_session_id(session), instance.get_id())
|
||||
key = (InstancesManager.get_session_id(session), instance.get_id())
|
||||
|
||||
if isinstance(instance, SingleInstance) and key in InstancesManager.instances:
|
||||
raise DuplicateInstanceError(instance)
|
||||
@@ -89,48 +136,27 @@ class InstancesManager:
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def get(session: dict, instance_id: str, instance_type: type = None, parent: BaseInstance = None, *args, **kwargs):
|
||||
def get(session: dict, instance_id: str):
|
||||
"""
|
||||
Get or create an instance of the given type (from its id)
|
||||
:param session:
|
||||
:param instance_id:
|
||||
:param instance_type:
|
||||
:param parent:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
key = (InstancesManager._get_session_id(session), instance_id)
|
||||
|
||||
return InstancesManager.instances[key]
|
||||
except KeyError:
|
||||
if instance_type:
|
||||
if not issubclass(instance_type, SingleInstance):
|
||||
assert parent is not None, "Parent instance must be provided if not SingleInstance"
|
||||
|
||||
if isinstance(parent, MultipleInstance):
|
||||
return instance_type(parent, _id=instance_id, *args, **kwargs)
|
||||
else:
|
||||
return instance_type(session, parent=parent, *args, **kwargs) # it will be automatically registered
|
||||
else:
|
||||
raise
|
||||
key = (InstancesManager.get_session_id(session), instance_id)
|
||||
return InstancesManager.instances[key]
|
||||
|
||||
@staticmethod
|
||||
def _get_session_id(session):
|
||||
if not session:
|
||||
def get_session_id(session):
|
||||
if session is None:
|
||||
return "** NOT LOGGED IN **"
|
||||
if "user_info" not in session:
|
||||
return "** UNKNOWN USER **"
|
||||
return session["user_info"].get("id", "** INVALID SESSION **")
|
||||
|
||||
@staticmethod
|
||||
def get_auth_proxy():
|
||||
return InstancesManager.get(special_session, Ids.AuthProxy)
|
||||
|
||||
@staticmethod
|
||||
def reset():
|
||||
return InstancesManager.instances.clear()
|
||||
InstancesManager.instances.clear()
|
||||
|
||||
|
||||
RootInstance = SingleInstance(special_session, Ids.Root, None)
|
||||
RootInstance = SingleInstance(None, special_session, Ids.Root)
|
||||
|
||||
@@ -144,50 +144,34 @@ def from_tree_with_metadata(
|
||||
|
||||
def from_parent_child_list(
|
||||
items: list,
|
||||
id_getter: callable = None,
|
||||
label_getter: callable = None,
|
||||
parent_getter: callable = None,
|
||||
ghost_color: str = "#ff9999"
|
||||
id_getter: Callable = None,
|
||||
label_getter: Callable = None,
|
||||
parent_getter: Callable = None,
|
||||
ghost_color: str = "#ff9999",
|
||||
root_color: str | None = "#ff9999"
|
||||
) -> tuple[list, list]:
|
||||
"""
|
||||
Convert a list of items with parent references to vis.js nodes and edges format.
|
||||
|
||||
Args:
|
||||
items: List of items (dicts or objects) with parent references
|
||||
(e.g., [{"id": "child", "parent": "root", "label": "Child"}, ...])
|
||||
id_getter: Optional callback to extract node ID from item
|
||||
Default: lambda item: item.get("id")
|
||||
label_getter: Optional callback to extract node label from item
|
||||
Default: lambda item: item.get("label", "")
|
||||
parent_getter: Optional callback to extract parent ID from item
|
||||
Default: lambda item: item.get("parent")
|
||||
ghost_color: Color to use for ghost nodes (nodes referenced as parents but not in list)
|
||||
Default: "#ff9999" (light red)
|
||||
id_getter: callback to extract node ID
|
||||
label_getter: callback to extract node label
|
||||
parent_getter: callback to extract parent ID
|
||||
ghost_color: color for ghost nodes (referenced parents)
|
||||
root_color: color for root nodes (nodes without parent)
|
||||
|
||||
Returns:
|
||||
tuple: (nodes, edges) where:
|
||||
- nodes: list of dicts with IDs from items, ghost nodes have color property
|
||||
- edges: list of dicts with 'from' and 'to' keys
|
||||
|
||||
Note:
|
||||
- Nodes with parent=None or parent="" are treated as root nodes
|
||||
- If a parent is referenced but doesn't exist in items, a ghost node is created
|
||||
with the ghost_color applied
|
||||
|
||||
Example:
|
||||
>>> items = [
|
||||
... {"id": "root", "label": "Root"},
|
||||
... {"id": "child1", "parent": "root", "label": "Child 1"},
|
||||
... {"id": "child2", "parent": "unknown", "label": "Child 2"}
|
||||
... ]
|
||||
>>> nodes, edges = from_parent_child_list(items)
|
||||
>>> # "unknown" will be created as a ghost node with color="#ff9999"
|
||||
tuple: (nodes, edges)
|
||||
"""
|
||||
|
||||
# Default getters
|
||||
if id_getter is None:
|
||||
id_getter = lambda item: item.get("id")
|
||||
|
||||
if label_getter is None:
|
||||
label_getter = lambda item: item.get("label", "")
|
||||
|
||||
if parent_getter is None:
|
||||
parent_getter = lambda item: item.get("parent")
|
||||
|
||||
@@ -205,34 +189,48 @@ def from_parent_child_list(
|
||||
existing_ids.add(node_id)
|
||||
nodes.append({
|
||||
"id": node_id,
|
||||
"label": node_label
|
||||
"label": node_label,
|
||||
# root color assigned later
|
||||
})
|
||||
|
||||
# Track ghost nodes to avoid duplicates
|
||||
# Track ghost nodes
|
||||
ghost_nodes = set()
|
||||
|
||||
# Second pass: create edges and identify ghost nodes
|
||||
# Track which nodes have parents
|
||||
nodes_with_parent = set()
|
||||
|
||||
# Second pass: create edges and detect ghost nodes
|
||||
for item in items:
|
||||
node_id = id_getter(item)
|
||||
parent_id = parent_getter(item)
|
||||
|
||||
# Skip if no parent or parent is empty string or None
|
||||
# Skip roots
|
||||
if parent_id is None or parent_id == "":
|
||||
continue
|
||||
|
||||
# Create edge from parent to child
|
||||
# Child has a parent
|
||||
nodes_with_parent.add(node_id)
|
||||
|
||||
# Create edge parent → child
|
||||
edges.append({
|
||||
"from": parent_id,
|
||||
"to": node_id
|
||||
})
|
||||
|
||||
# Check if parent exists, if not create ghost node
|
||||
# Create ghost node if parent not found
|
||||
if parent_id not in existing_ids and parent_id not in ghost_nodes:
|
||||
ghost_nodes.add(parent_id)
|
||||
nodes.append({
|
||||
"id": parent_id,
|
||||
"label": str(parent_id), # Use ID as label for ghost nodes
|
||||
"label": str(parent_id),
|
||||
"color": ghost_color
|
||||
})
|
||||
|
||||
# Final pass: assign color to root nodes
|
||||
if root_color is not None:
|
||||
for node in nodes:
|
||||
if node["id"] not in nodes_with_parent and node["id"] not in ghost_nodes:
|
||||
# Root node
|
||||
node["color"] = root_color
|
||||
|
||||
return nodes, edges
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from bs4 import Tag
|
||||
from fastcore.xml import FT
|
||||
@@ -234,6 +235,18 @@ def get_id(obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""Convert a PascalCase or CamelCase string to snake_case."""
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
name = name.strip()
|
||||
# Insert underscore before capital letters (except the first one)
|
||||
s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
# Handle consecutive capital letters (like 'HTTPServer' -> 'http_server')
|
||||
s2 = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s1)
|
||||
return s2.lower()
|
||||
|
||||
@utils_rt(Routes.Commands)
|
||||
def post(session, c_id: str, client_response: dict = None):
|
||||
"""
|
||||
|
||||
@@ -11,6 +11,10 @@ import re
|
||||
|
||||
def pascal_to_snake(name: str) -> str:
|
||||
"""Convert a PascalCase or CamelCase string to snake_case."""
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
name = name.strip()
|
||||
# Insert underscore before capital letters (except the first one)
|
||||
s1 = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
# Handle consecutive capital letters (like 'HTTPServer' -> 'http_server')
|
||||
|
||||
@@ -10,6 +10,7 @@ from starlette.responses import Response
|
||||
from myfasthtml.auth.routes import setup_auth_routes
|
||||
from myfasthtml.auth.utils import create_auth_beforeware
|
||||
from myfasthtml.core.AuthProxy import AuthProxy
|
||||
from myfasthtml.core.instances import RootInstance
|
||||
from myfasthtml.core.utils import utils_app
|
||||
|
||||
logger = logging.getLogger("MyFastHtml")
|
||||
@@ -104,6 +105,6 @@ def create_app(daisyui: Optional[bool] = True,
|
||||
setup_auth_routes(app, rt, base_url=base_url)
|
||||
|
||||
# create the AuthProxy instance
|
||||
AuthProxy(base_url) # using the auto register mechanism to expose it
|
||||
AuthProxy(RootInstance, base_url) # using the auto register mechanism to expose it
|
||||
|
||||
return app, rt
|
||||
|
||||
Reference in New Issue
Block a user