Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: core managed components functionality #279

Merged
merged 21 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a7a98e9
feat!: core managed components functionality
mmikita95 Mar 4, 2024
762b144
fix: moving component id generation out of class
mmikita95 Mar 5, 2024
35c3347
chore: updates per review
mmikita95 Mar 6, 2024
cd73b29
feat: initialize CMCs outside of handlers
mmikita95 Mar 7, 2024
c17fc6b
fix: enable manual positioning
mmikita95 Mar 7, 2024
271c02b
fix: logic for manual positioning
mmikita95 Mar 7, 2024
ca49806
fix: missing default None for pop in _create_component
mmikita95 Mar 7, 2024
94eed2b
chore: populate content from props; UI manager generated methods
mmikita95 Mar 7, 2024
d216691
fix: from typing import Unpack => from typing_extensions import Unpack
mmikita95 Mar 7, 2024
fa2cfa4
fix: updated tests
mmikita95 Mar 7, 2024
aa56c8b
Merge branch 'streamsync-cloud:dev' into feat-code-managed-components…
mmikita95 Mar 8, 2024
969fb51
feat: added ui code generator
raaymax Mar 8, 2024
da47b50
Merge branch 'streamsync-cloud:dev' into feat-code-managed-components…
mmikita95 Mar 11, 2024
59888cd
chore: updating UI manager to work with generated components
mmikita95 Mar 11, 2024
e3a3d0a
Merge pull request #1 from raaymax/ui-code-generator
mmikita95 Mar 11, 2024
f1f7216
style: linter errors fixes
raaymax Mar 11, 2024
75a3d25
Merge pull request #2 from raaymax/eslint-fixes
mmikita95 Mar 11, 2024
19fc17c
fix: updating base component tree during component update
mmikita95 Mar 11, 2024
995f6c1
fix: session tree class property for root instead of override to sati…
mmikita95 Mar 11, 2024
f62ef1e
fix: attach_root argument for component tree init
mmikita95 Mar 11, 2024
6fec1bb
fix: apprunner tree update not necessary
mmikita95 Mar 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions docs/docs/component-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,8 @@ outline: [2, 2]
---

<script setup>
import { generateCore } from "../../ui/src/core"
const ss = generateCore();
const types = ss.getSupportedComponentTypes();
const defs = types.map(type => {
const def = ss.getComponentDefinition(type);
return {
type,
name: def.name,
docs: def.docs,
description: def.description,
fields: def.fields,
events: def.events,
category: def.category
}
});
import defs from "streamsync-ui/components.json";

const categories = {
"Layout": "Components to organise the app's layout. Not meaningful by themselves; their objective is to enhance how other components are presented.",
"Content": "Components that present content and are meaningful by themselves. For example, charts, images or text.",
Expand Down
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"preview": "vitepress preview docs"
},
"dependencies": {
"streamsync-ui": "*",
"vitepress": "^1.0.0-rc.44",
"vue": "^3.4.21"
}
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions src/streamsync/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,14 @@ def _main(self) -> None:

terminate_early = False

try:
streamsync.base_component_tree.ingest(self.bmc_components)
except BaseException:
streamsync.initial_state.add_log_entry(
"error", "UI Components Error", "Couldn't load components. An exception was raised.", tb.format_exc())
if self.mode == "run":
terminate_early = True

try:
self._execute_user_code()
except BaseException:
Expand All @@ -337,14 +345,6 @@ def _main(self) -> None:
if self.mode == "run":
terminate_early = True

try:
streamsync.base_component_tree.ingest(self.bmc_components)
except BaseException:
streamsync.initial_state.add_log_entry(
"error", "UI Components Error", "Couldn't load components. An exception was raised.", tb.format_exc())
if self.mode == "run":
terminate_early = True

if terminate_early:
self._terminate_early()
return
Expand Down Expand Up @@ -671,7 +671,7 @@ async def init_session(self, payload: InitSessionRequestPayload) -> AppProcessSe
return await self.dispatch_message(None, InitSessionRequest(
type="sessionInit",
payload=payload
))
))

async def update_components(self, session_id: str, payload: ComponentUpdateRequestPayload) -> AppProcessServerResponse:
if self.mode != "edit":
Expand Down
102 changes: 5 additions & 97 deletions src/streamsync/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import logging
import secrets
import sys
import threading
import time
import traceback
from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union
Expand All @@ -17,7 +16,7 @@
import json
import math
from streamsync.ss_types import Readable, InstancePath, StreamsyncEvent, StreamsyncEventResult, StreamsyncFileItem
from pydantic import BaseModel, Field
from streamsync.core_ui import ComponentTree, SessionComponentTree


class Config:
Expand Down Expand Up @@ -474,101 +473,6 @@ def call_frontend_function(self, module_key: str, function_name: str, args: List
})


# TODO Consider switching Component to use Pydantic

class Component(BaseModel):
id: str
type: str
content: Dict[str, str] = Field(default_factory=dict)
flag: Optional[str] = None
position: int = 0
parentId: Optional[str] = None
handlers: Optional[Dict[str, str]] = None
visible: Optional[Union[bool, str]] = None
binding: Optional[Dict] = None

def to_dict(self) -> Dict:
"""
Wrapper for model_dump to ensure backward compatibility.
"""
return self.model_dump(exclude_none=True)


class ComponentTree:

def __init__(self) -> None:
self.counter: int = 0
self.components: Dict[str, Component] = {}
root_component = Component(
id="root", type="root", content={}
)
self.attach(root_component)

def get_component(self, component_id: str) -> Optional[Component]:
return self.components.get(component_id)

def get_descendents(self, parent_id: str) -> List[Component]:
children = list(filter(lambda c: c.parentId == parent_id,
self.components.values()))
desc = children.copy()
for child in children:
desc += self.get_descendents(child.id)

return desc

def attach(self, component: Component) -> None:
self.counter += 1
self.components[component.id] = component

def ingest(self, serialised_components: Dict[str, Any]) -> None:
removed_ids = self.components.keys() - serialised_components.keys()

for component_id in removed_ids:
if component_id == "root":
continue
self.components.pop(component_id)
for component_id, sc in serialised_components.items():
component = Component(**sc)
self.components[component_id] = component

def to_dict(self) -> Dict:
active_components = {}
for id, component in self.components.items():
active_components[id] = component.to_dict()
return active_components


class SessionComponentTree(ComponentTree):

def __init__(self, base_component_tree: ComponentTree):
super().__init__()
self.base_component_tree = base_component_tree

def get_component(self, component_id: str) -> Optional[Component]:
# Check if session component tree contains requested key
session_component_present = component_id in self.components

if session_component_present:
# If present, return session component (even if it's None)
session_component = self.components.get(component_id)
return session_component

# Otherwise, try to obtain the base tree component
return self.base_component_tree.get_component(component_id)

def to_dict(self) -> Dict:
active_components = {
# Collecting serialized base tree components
component_id: base_component.to_dict()
for component_id, base_component
in self.base_component_tree.components.items()
}
for component_id, session_component in self.components.items():
# Overriding base tree components with session-specific ones
active_components[component_id] = session_component.to_dict()
return active_components


class EventDeserialiser:

"""Applies transformations to the payload of an incoming event, depending on its type.
Expand Down Expand Up @@ -1079,6 +983,10 @@ def _call_handler_callable(self, event_type, target_component, instance_path, pa
"headers": self.session.headers
}
arg_values.append(session_info)
elif arg == "ui":
from streamsync.ui import StreamsyncUIManager
ui_manager = StreamsyncUIManager(self.session.session_component_tree)
arg_values.append(ui_manager)

result = None
if is_async_handler:
Expand Down
148 changes: 148 additions & 0 deletions src/streamsync/core_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from contextvars import ContextVar
from typing import Any, Dict, List, Optional, Union
import uuid

from pydantic import BaseModel, Field

current_parent_container: ContextVar[Union["Component", None]] = \
ContextVar("current_parent_container")
# This variable is thread safe and context safe


def generate_component_id():
return str(uuid.uuid4())


class Component(BaseModel):
id: str = Field(default_factory=generate_component_id)
type: str
content: Dict[str, Any] = Field(default_factory=dict)
flag: Optional[str] = None
position: int = 0
parentId: Optional[str] = None
handlers: Optional[Dict[str, str]] = None
visible: Optional[Union[bool, str]] = None
binding: Optional[Dict] = None

def to_dict(self) -> Dict:
"""
Wrapper for model_dump to ensure backward compatibility.
"""
return self.model_dump(exclude_none=True)

def __enter__(self) -> "Component":
self._token = current_parent_container.set(self)
return self

def __exit__(self, *_):
current_parent_container.reset(self._token)


class ComponentTree:

def __init__(self, attach_root=True) -> None:
self.counter: int = 0
self.components: Dict[str, Component] = {}
if attach_root:
root_component = Component(
id="root", type="root", content={}
)
self.attach(root_component)

def get_component(self, component_id: str) -> Optional[Component]:
return self.components.get(component_id)

def get_direct_descendents(self, parent_id: str) -> List[Component]:
children = list(filter(lambda c: c.parentId == parent_id,
self.components.values()))
return children

def get_direct_descendents_length(self, parent_id):
return len(self.get_direct_descendents(parent_id))

def get_descendents(self, parent_id: str) -> List[Component]:
children = self.get_direct_descendents(parent_id)
desc = children.copy()
for child in children:
desc += self.get_descendents(child.id)

return desc

def determine_position(self, _: str, parent_id: str, is_positionless: bool = False):
if is_positionless:
return -2

children = self.get_direct_descendents(parent_id)
if len(children) > 0:
position = max([0, max([child.position for child in children]) + 1])
return position
else:
return 0

def attach(self, component: Component, override=False) -> None:
self.counter += 1
if (component.id in self.components) and (override is False):
raise RuntimeWarning(f"Component with ID {component.id} already exists")
self.components[component.id] = component

def ingest(self, serialised_components: Dict[str, Any]) -> None:
removed_ids = self.components.keys() - serialised_components.keys()

for component_id in removed_ids:
if component_id == "root":
continue
self.components.pop(component_id)
for component_id, sc in serialised_components.items():
component = Component(**sc)
self.components[component_id] = component

def to_dict(self) -> Dict:
active_components = {}
for id, component in self.components.items():
active_components[id] = component.to_dict()
return active_components


class SessionComponentTree(ComponentTree):

def __init__(self, base_component_tree: ComponentTree):
super().__init__(attach_root=False)
self.base_component_tree = base_component_tree

def determine_position(self, component_id: str, parent_id: str, is_positionless: bool = False):
session_component_present = component_id in self.components
if session_component_present:
# If present, use ComponentTree method
# for determining position directly from this class
return super().determine_position(component_id, parent_id, is_positionless)
else:
# Otherwise, invoke it on base component tree
return self.base_component_tree.determine_position(component_id, parent_id, is_positionless)

def get_component(self, component_id: str) -> Optional[Component]:
# Check if session component tree contains requested key
session_component_present = component_id in self.components

if session_component_present:
# If present, return session component (even if it's None)
session_component = self.components.get(component_id)
return session_component

# Otherwise, try to obtain the base tree component
return self.base_component_tree.get_component(component_id)

def to_dict(self) -> Dict:
active_components = {
# Collecting serialized base tree components
component_id: base_component.to_dict()
for component_id, base_component
in self.base_component_tree.components.items()
}
for component_id, session_component in self.components.items():
# Overriding base tree components with session-specific ones
active_components[component_id] = session_component.to_dict()
return active_components


class UIError(Exception):
...
2 changes: 1 addition & 1 deletion src/streamsync/ss_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class EventResponsePayload(BaseModel):
result: Any
mutations: Dict[str, Any]
mail: List
components: Dict
components: Optional[Dict] = None


class StateEnquiryResponsePayload(BaseModel):
Expand Down
Loading
Loading