From 8f914f385d8e503985dfa458e0b02196fd3e76c0 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Wed, 2 Oct 2024 02:32:19 +0800 Subject: [PATCH] [JAC-CLOUD]: Adjust to plugin migration --- jac-cloud/jac_cloud/core/architype.py | 265 +--------- jac-cloud/jac_cloud/core/context.py | 4 - jac-cloud/jac_cloud/core/memory.py | 7 +- jac-cloud/jac_cloud/plugin/jaseci.py | 551 ++++++++++++++++----- jac-cloud/jac_cloud/tests/simple_graph.jac | 26 +- 5 files changed, 457 insertions(+), 396 deletions(-) diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index d08a2bfff8..9e4ae7d403 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -7,7 +7,6 @@ from re import IGNORECASE, compile from typing import ( Any, - Callable, ClassVar, Iterable, Mapping, @@ -20,7 +19,6 @@ from bson import ObjectId -from jaclang.compiler.constant import EdgeDir from jaclang.runtimelib.architype import ( Access as _Access, AccessLevel, @@ -389,43 +387,6 @@ def disconnect_edge(self, anchor: Anchor) -> None: """Push update that there's edge that has been removed.""" self.pull("edges", anchor) - def allow_root( - self, root: "BaseAnchor", level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Allow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - if (ref_id := root.ref_id) and level != access.anchors.get( - ref_id, AccessLevel.NO_ACCESS - ): - access.anchors[ref_id] = level - self._set.update({f"access.roots.anchors.{ref_id}": level.name}) - self._unset.pop(f"access.roots.anchors.{ref_id}", None) - - def disallow_root( - self, root: Anchor, level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Disallow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - - if (ref_id := root.ref_id) and access.anchors.pop(ref_id, None) is not None: - self._unset.update({f"access.roots.anchors.{ref_id}": True}) - self._set.pop(f"access.roots.anchors.{ref_id}", None) - - def unrestrict(self, level: AccessLevel | int | str = AccessLevel.READ) -> None: - """Allow everyone to access current Architype.""" - level = AccessLevel.cast(level) - if level != self.access.all: - self.access.all = level - self._set.update({"access.all": level.name}) - - def restrict(self) -> None: - """Disallow others to access current Architype.""" - if self.access.all > AccessLevel.NO_ACCESS: - self.access.all = AccessLevel.NO_ACCESS - self._set.update({"access.all": AccessLevel.NO_ACCESS.name}) - #################################################### # POPULATE OPERATIONS # #################################################### @@ -461,14 +422,16 @@ def build_query( bulk_write: BulkWrite, ) -> None: """Save Anchor.""" - if self.state.deleted is False and self.has_write_access(self): # type: ignore[attr-defined] + from jaclang.plugin.feature import JacFeature as Jac + + if self.state.deleted is False and Jac.check_write_access(self): # type: ignore[arg-type] self.state.deleted = True self.delete(bulk_write) elif not self.state.connected: self.state.connected = True self.sync_hash() self.insert(bulk_write) - elif self.has_connect_access(self): # type: ignore[attr-defined] + elif Jac.check_connect_access(self): # type: ignore[arg-type] self.update(bulk_write, True) def apply(self, session: ClientSession | None = None) -> BulkWrite: @@ -507,9 +470,9 @@ def update(self, bulk_write: BulkWrite, propagate: bool = False) -> None: ############################################################ # POPULATE CONTEXT # ############################################################ - from .context import JaseciContext + from jaclang.plugin.feature import JacFeature as Jac - if JaseciContext.get().root.has_write_access(self): + if Jac.check_write_access(self): # type: ignore[arg-type] set_architype = changes.pop("$set", {}) if is_dataclass(architype := self.architype) and not isinstance( architype, type @@ -587,10 +550,6 @@ def delete(self, bulk_write: BulkWrite) -> None: """Append Delete Query.""" raise NotImplementedError("delete must be implemented in subclasses") - def destroy(self) -> None: - """Delete Anchor.""" - raise NotImplementedError("destroy must be implemented in subclasses") - def has_changed(self) -> int: """Check if needs to update.""" if self.state.full_hash != (new_hash := hash(pdumps(self.serialize()))): @@ -608,49 +567,6 @@ def sync_hash(self) -> None: } self.state.full_hash = hash(pdumps(self.serialize())) - def access_level(self, to: Anchor) -> AccessLevel: - """Access validation.""" - if not to.persistent: - return AccessLevel.WRITE - - from .context import JaseciContext - - jctx = JaseciContext.get() - - jroot = jctx.root - - # if current root is system_root - # if current root id is equal to target anchor's root id - # if current root is the target anchor - if jroot == jctx.system_root or jroot.id == to.root or jroot == to: - return AccessLevel.WRITE - - access_level = AccessLevel.NO_ACCESS - - # if target anchor have set access.all - if (to_access := to.access).all > AccessLevel.NO_ACCESS: - access_level = to_access.all - - # if target anchor's root have set allowed roots - # if current root is allowed to the whole graph of target anchor's root - if to.root and isinstance( - to_root := jctx.mem.find_by_id(NodeAnchor.ref(f"n::{to.root}")), Anchor - ): - if to_root.access.all > access_level: - access_level = to_root.access.all - - level = to_root.access.roots.check(jroot.ref_id) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - # if target anchor have set allowed roots - # if current root is allowed to target anchor - level = to_access.roots.check(jroot.ref_id) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - return access_level - # ---------------------------------------------------------------------- # def report(self) -> dict[str, object]: @@ -760,47 +676,6 @@ def delete(self, bulk_write: BulkWrite) -> None: bulk_write.del_node(self.id) - def destroy(self) -> None: - """Delete Anchor.""" - if self.state.deleted is None: - from .context import JaseciContext - - jctx = JaseciContext.get() - - if jctx.root.has_write_access(self): - - self.state.deleted = False - - for edge in self.edges: - edge.destroy() - jctx.mem.remove(self.id) - - def get_edges( - self, - dir: EdgeDir, - filter_func: Callable[[list["EdgeArchitype"]], list["EdgeArchitype"]] | None, - target_obj: list["NodeArchitype"] | None, - ) -> list["EdgeArchitype"]: - """Get edges connected to this node.""" - from .context import JaseciContext - - JaseciContext.get().mem.populate_data(self.edges) - - return super().get_edges(dir, filter_func, target_obj) - - def edges_to_nodes( - self, - dir: EdgeDir, - filter_func: Callable[[list["EdgeArchitype"]], list["EdgeArchitype"]] | None, - target_obj: list["NodeArchitype"] | None, - ) -> list["NodeArchitype"]: - """Get set of nodes connected to this node.""" - from .context import JaseciContext - - JaseciContext.get().mem.populate_data(self.edges) - - return super().edges_to_nodes(dir, filter_func, target_obj) - def serialize(self) -> dict[str, object]: """Serialize Node Anchor.""" return { @@ -848,11 +723,6 @@ def __document__(cls, doc: Mapping[str, Any]) -> "EdgeAnchor": anchor.sync_hash() return anchor - def __post_init__(self) -> None: - """Populate edge to source and target.""" - # remove parent trigger - pass - @classmethod def ref(cls, ref_id: str) -> "EdgeAnchor": """Return EdgeAnchor instance if existing.""" @@ -883,25 +753,6 @@ def delete(self, bulk_write: BulkWrite) -> None: bulk_write.del_edge(self.id) - def destroy(self) -> None: - """Delete Anchor.""" - if self.state.deleted is None: - from .context import JaseciContext - - jctx = JaseciContext.get() - - if jctx.root.has_write_access(self): - self.state.deleted = False - self.detach() - jctx.mem.remove(self.id) - - def detach(self) -> None: - """Detach edge from nodes.""" - self.source.remove_edge(self) - self.source.disconnect_edge(self) - self.target.remove_edge(self) - self.target.disconnect_edge(self) - def serialize(self) -> dict[str, object]: """Serialize Node Anchor.""" return { @@ -972,61 +823,6 @@ def delete(self, bulk_write: BulkWrite) -> None: """Append Delete Query.""" bulk_write.del_walker(self.id) - def destroy(self) -> None: - """Delete Anchor.""" - if self.state.deleted is None: - from .context import JaseciContext - - jctx = JaseciContext.get() - - if jctx.root.has_write_access(self): - self.state.deleted = False - jctx.mem.remove(self.id) - - def spawn_call(self, node: Anchor) -> "WalkerArchitype": - """Invoke data spatial call.""" - if walker := self.architype: - self.path = [] - self.next = [node] - self.returns = [] - while len(self.next): - if current_node := self.next.pop(0).architype: - for i in current_node._jac_entry_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - self.returns.append(i.func(current_node, walker)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_entry_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func: - self.returns.append(i.func(walker, current_node)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_exit_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func: - self.returns.append(i.func(walker, current_node)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in current_node._jac_exit_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - self.returns.append(i.func(current_node, walker)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - self.ignores = [] - return walker - raise Exception(f"Invalid Reference {self.id}") - @dataclass(eq=False, repr=False, kw_only=True) class ObjectAnchor(BaseAnchor, Anchor): # type: ignore[misc] @@ -1081,7 +877,7 @@ class NodeArchitype(BaseArchitype, _NodeArchitype): __jac__: NodeAnchor - def __post_init__(self) -> None: + def __init__(self) -> None: """Create node architype.""" self.__jac__ = NodeAnchor( architype=self, @@ -1102,27 +898,6 @@ class EdgeArchitype(BaseArchitype, _EdgeArchitype): __jac__: EdgeAnchor - def __attach__( - self, - source: NodeAnchor, - target: NodeAnchor, - is_undirected: bool, - ) -> None: - """Attach EdgeAnchor properly.""" - jac = self.__jac__ = EdgeAnchor( - architype=self, - name=self.__class__.__name__, - source=source, - target=target, - is_undirected=is_undirected, - access=Permission(), - state=AnchorState(), - ) - source.edges.append(jac) - target.edges.append(jac) - source.connect_edge(jac) - target.connect_edge(jac) - @classmethod def __ref_cls__(cls) -> str: """Get class naming.""" @@ -1134,7 +909,7 @@ class WalkerArchitype(BaseArchitype, _WalkerArchitype): __jac__: WalkerAnchor - def __post_init__(self) -> None: + def __init__(self) -> None: """Create walker architype.""" self.__jac__ = WalkerAnchor( architype=self, @@ -1154,7 +929,7 @@ class ObjectArchitype(BaseArchitype, Architype): __jac__: ObjectAnchor - def __post_init__(self) -> None: + def __init__(self) -> None: """Create default architype.""" self.__jac__ = ObjectAnchor( architype=self, @@ -1171,26 +946,6 @@ class GenericEdge(EdgeArchitype): _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] - def __attach__( - self, - source: NodeAnchor, - target: NodeAnchor, - is_undirected: bool, - ) -> None: - """Attach EdgeAnchor properly.""" - jac = self.__jac__ = EdgeAnchor( - architype=self, - source=source, - target=target, - is_undirected=is_undirected, - access=Permission(), - state=AnchorState(), - ) - source.edges.append(jac) - target.edges.append(jac) - source.connect_edge(jac) - target.connect_edge(jac) - @dataclass(eq=False) class Root(NodeArchitype): @@ -1199,7 +954,7 @@ class Root(NodeArchitype): _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] - def __post_init__(self) -> None: + def __init__(self) -> None: """Create node architype.""" self.__jac__ = NodeAnchor( architype=self, diff --git a/jac-cloud/jac_cloud/core/context.py b/jac-cloud/jac_cloud/core/context.py index 5c87d63bd7..4b96a2b813 100644 --- a/jac-cloud/jac_cloud/core/context.py +++ b/jac-cloud/jac_cloud/core/context.py @@ -54,10 +54,6 @@ class JaseciContext(ExecutionContext): base: ExecutionContext request: Request - def validate_access(self) -> bool: - """Validate access.""" - return self.root.has_read_access(self.entry_node) - def close(self) -> None: """Clean up context.""" self.mem.close() diff --git a/jac-cloud/jac_cloud/core/memory.py b/jac-cloud/jac_cloud/core/memory.py index e362f77bdd..4be9d0d8fa 100644 --- a/jac-cloud/jac_cloud/core/memory.py +++ b/jac-cloud/jac_cloud/core/memory.py @@ -115,9 +115,8 @@ def close(self) -> None: def get_bulk_write(self) -> BulkWrite: """Sync memory to database.""" - from .context import JaseciContext + from jaclang.plugin.feature import JacFeature as Jac - JaseciContext bulk_write = BulkWrite() for anchor in self.__gc__: @@ -139,8 +138,8 @@ def get_bulk_write(self) -> BulkWrite: bulk_write.operations[anchor.__class__].append( InsertOne(anchor.serialize()) ) - elif (new_hash := anchor.has_changed()) and anchor.has_connect_access( - anchor + elif (new_hash := anchor.has_changed()) and Jac.check_connect_access( + anchor # type: ignore[arg-type] ): anchor.state.full_hash = new_hash if ( diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index 12588428c7..d05805a1e2 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -21,7 +21,8 @@ ) from fastapi.responses import ORJSONResponse -from jaclang.plugin.default import JacFeatureDefaults, hookimpl +from jaclang.compiler.constant import EdgeDir +from jaclang.plugin.default import JacFeatureImpl, hookimpl from jaclang.plugin.feature import JacFeature as Jac from jaclang.runtimelib.architype import DSFunc @@ -32,14 +33,18 @@ from starlette.datastructures import UploadFile as BaseUploadFile from ..core.architype import ( + AccessLevel, Anchor, + AnchorState, Architype, BaseAnchor, + EdgeAnchor, EdgeArchitype, GenericEdge, NodeAnchor, NodeArchitype, ObjectArchitype, + Permission, Root, WalkerAnchor, WalkerArchitype, @@ -160,8 +165,8 @@ def api_entry( jctx = JaseciContext.create(request, NodeAnchor.ref(node) if node else None) wlk: WalkerAnchor = cls(**body, **pl["query"], **pl["files"]).__jac__ - if jctx.validate_access(): - wlk.spawn_call(jctx.entry_node) + if Jac.check_read_access(jctx.entry_node): + Jac.spawn_call(wlk.architype, jctx.entry_node.architype) jctx.close() return jctx.response(wlk.returns) else: @@ -244,16 +249,201 @@ class DefaultSpecs: private: bool = False -class JacPlugin: +class JacAccessValidationPlugin: + """Jac Access Validation Implementations.""" + + @staticmethod + @hookimpl + def allow_root( + architype: Architype, root_id: BaseAnchor, level: AccessLevel | int | str + ) -> None: + """Allow all access from target root graph to current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.allow_root( + architype=architype, root_id=root_id, level=level # type: ignore[arg-type] + ) + return + + anchor = architype.__jac__ + + level = AccessLevel.cast(level) + access = anchor.access.roots + if ( + isinstance(anchor, BaseAnchor) + and (ref_id := root_id.ref_id) + and level != access.anchors.get(ref_id, AccessLevel.NO_ACCESS) + ): + access.anchors[ref_id] = level + anchor._set.update({f"access.roots.anchors.{ref_id}": level.name}) + anchor._unset.pop(f"access.roots.anchors.{ref_id}", None) + + @staticmethod + @hookimpl + def disallow_root( + architype: Architype, root_id: BaseAnchor, level: AccessLevel | int | str + ) -> None: + """Disallow all access from target root graph to current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.disallow_root( + architype=architype, root_id=root_id, level=level # type: ignore[arg-type] + ) + return + + anchor = architype.__jac__ + + level = AccessLevel.cast(level) + access = anchor.access.roots + if ( + isinstance(anchor, BaseAnchor) + and (ref_id := root_id.ref_id) + and access.anchors.pop(ref_id, None) is not None + ): + anchor._unset.update({f"access.roots.anchors.{ref_id}": True}) + anchor._set.pop(f"access.roots.anchors.{ref_id}", None) + + @staticmethod + @hookimpl + def unrestrict(architype: Architype, level: AccessLevel | int | str) -> None: + """Allow everyone to access current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.unrestrict(architype=architype, level=level) + return + + anchor = architype.__jac__ + + level = AccessLevel.cast(level) + if isinstance(anchor, BaseAnchor) and level != anchor.access.all: + anchor.access.all = level + anchor._set.update({"access.all": level.name}) + + @staticmethod + @hookimpl + def restrict(architype: Architype) -> None: + """Disallow others to access current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.restrict(architype=architype) + return + + anchor = architype.__jac__ + + if isinstance(anchor, BaseAnchor) and anchor.access.all > AccessLevel.NO_ACCESS: + anchor.access.all = AccessLevel.NO_ACCESS + anchor._set.update({"access.all": AccessLevel.NO_ACCESS.name}) + + @staticmethod + @hookimpl + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.check_access_level(to=to) + + if not to.persistent: + return AccessLevel.WRITE + + from ..core.context import JaseciContext + + jctx = JaseciContext.get() + + jroot = jctx.root + + # if current root is system_root + # if current root id is equal to target anchor's root id + # if current root is the target anchor + if jroot == jctx.system_root or jroot.id == to.root or jroot == to: + return AccessLevel.WRITE + + access_level = AccessLevel.NO_ACCESS + + # if target anchor have set access.all + if (to_access := to.access).all > AccessLevel.NO_ACCESS: + access_level = to_access.all + + # if target anchor's root have set allowed roots + # if current root is allowed to the whole graph of target anchor's root + if to.root and isinstance( + to_root := jctx.mem.find_by_id(NodeAnchor.ref(f"n::{to.root}")), Anchor + ): + if to_root.access.all > access_level: + access_level = to_root.access.all + + level = to_root.access.roots.check(jroot.ref_id) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + # if target anchor have set allowed roots + # if current root is allowed to target anchor + level = to_access.roots.check(jroot.ref_id) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + return access_level + + +class JacNodePlugin: + """Jac Node Operations.""" + + @staticmethod + @hookimpl + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Callable[[list[EdgeArchitype]], list[EdgeArchitype]] | None, + target_obj: list[NodeArchitype] | None, + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + if FastAPI.enable(): + JaseciContext.get().mem.populate_data(node.edges) + + return JacFeatureImpl.get_edges( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj # type: ignore[arg-type, return-value] + ) + + @staticmethod + @hookimpl + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Callable[[list[EdgeArchitype]], list[EdgeArchitype]] | None, + target_obj: list[NodeArchitype] | None, + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + if FastAPI.enable(): + JaseciContext.get().mem.populate_data(node.edges) + + return JacFeatureImpl.edges_to_nodes( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj # type: ignore[arg-type, return-value] + ) + + +class JacEdgePlugin: + """Jac Edge Operations.""" + + @staticmethod + @hookimpl + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.detach(edge=edge) + return + + Jac.remove_edge(node=edge.source, edge=edge) + edge.source.disconnect_edge(edge) + + Jac.remove_edge(node=edge.target, edge=edge) + edge.target.disconnect_edge(edge) + + +class JacPlugin(JacAccessValidationPlugin, JacNodePlugin, JacEdgePlugin): """Jaseci Implementations.""" @staticmethod @hookimpl def get_context() -> ExecutionContext: """Get current execution context.""" - if FastAPI.is_enabled(): - return JaseciContext.get() - return JacFeatureDefaults.get_context() + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_context() + + return JaseciContext.get() @staticmethod @hookimpl @@ -264,46 +454,46 @@ def make_architype( on_exit: list[DSFunc], ) -> Type[Architype]: """Create a new architype.""" - if FastAPI.is_enabled(): - for i in on_entry + on_exit: - i.resolve(cls) - if not hasattr(cls, "_jac_entry_funcs_") or not hasattr( - cls, "_jac_exit_funcs_" - ): - # Saving the module path and reassign it after creating cls - # So the jac modules are part of the correct module - cur_module = cls.__module__ - cls = type(cls.__name__, (cls, arch_base), {}) - cls.__module__ = cur_module - cls._jac_entry_funcs_ = on_entry # type: ignore - cls._jac_exit_funcs_ = on_exit # type: ignore - else: - new_entry_funcs = OrderedDict(zip([i.name for i in on_entry], on_entry)) - entry_funcs = OrderedDict( - zip([i.name for i in cls._jac_entry_funcs_], cls._jac_entry_funcs_) - ) - entry_funcs.update(new_entry_funcs) - cls._jac_entry_funcs_ = list(entry_funcs.values()) + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_architype( + cls=cls, arch_base=arch_base, on_entry=on_entry, on_exit=on_exit + ) + for i in on_entry + on_exit: + i.resolve(cls) + if not hasattr(cls, "_jac_entry_funcs_") or not hasattr( + cls, "_jac_exit_funcs_" + ): + # Saving the module path and reassign it after creating cls + # So the jac modules are part of the correct module + cur_module = cls.__module__ + cls = type(cls.__name__, (cls, arch_base), {}) + cls.__module__ = cur_module + cls._jac_entry_funcs_ = on_entry # type: ignore + cls._jac_exit_funcs_ = on_exit # type: ignore + else: + new_entry_funcs = OrderedDict(zip([i.name for i in on_entry], on_entry)) + entry_funcs = OrderedDict( + zip([i.name for i in cls._jac_entry_funcs_], cls._jac_entry_funcs_) + ) + entry_funcs.update(new_entry_funcs) + cls._jac_entry_funcs_ = list(entry_funcs.values()) - new_exit_funcs = OrderedDict(zip([i.name for i in on_exit], on_exit)) - exit_funcs = OrderedDict( - zip([i.name for i in cls._jac_exit_funcs_], cls._jac_exit_funcs_) - ) - exit_funcs.update(new_exit_funcs) - cls._jac_exit_funcs_ = list(exit_funcs.values()) + new_exit_funcs = OrderedDict(zip([i.name for i in on_exit], on_exit)) + exit_funcs = OrderedDict( + zip([i.name for i in cls._jac_exit_funcs_], cls._jac_exit_funcs_) + ) + exit_funcs.update(new_exit_funcs) + cls._jac_exit_funcs_ = list(exit_funcs.values()) - inner_init = cls.__init__ # type: ignore + inner_init = cls.__init__ # type: ignore - @wraps(inner_init) - def new_init(self: Architype, *args: object, **kwargs: object) -> None: - arch_base.__init__(self) - inner_init(self, *args, **kwargs) + @wraps(inner_init) + def new_init(self: Architype, *args: object, **kwargs: object) -> None: + arch_base.__init__(self) + inner_init(self, *args, **kwargs) - cls.__init__ = new_init # type: ignore - return cls - return JacFeatureDefaults.make_architype( - cls=cls, arch_base=arch_base, on_entry=on_entry, on_exit=on_exit - ) + cls.__init__ = new_init # type: ignore + return cls @staticmethod @hookimpl @@ -311,20 +501,20 @@ def make_obj( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a new architype.""" - if FastAPI.is_enabled(): - - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, - arch_base=ObjectArchitype, - on_entry=on_entry, - on_exit=on_exit, - ) - return cls + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_obj(on_entry=on_entry, on_exit=on_exit) + + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, + arch_base=ObjectArchitype, + on_entry=on_entry, + on_exit=on_exit, + ) + return cls - return decorator - return JacFeatureDefaults.make_obj(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl @@ -332,17 +522,17 @@ def make_node( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a obj architype.""" - if FastAPI.is_enabled(): + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_node(on_entry=on_entry, on_exit=on_exit) - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, arch_base=NodeArchitype, on_entry=on_entry, on_exit=on_exit - ) - return cls + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, arch_base=NodeArchitype, on_entry=on_entry, on_exit=on_exit + ) + return cls - return decorator - return JacFeatureDefaults.make_node(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl @@ -350,17 +540,17 @@ def make_edge( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a edge architype.""" - if FastAPI.is_enabled(): + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_edge(on_entry=on_entry, on_exit=on_exit) - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, arch_base=EdgeArchitype, on_entry=on_entry, on_exit=on_exit - ) - return cls + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, arch_base=EdgeArchitype, on_entry=on_entry, on_exit=on_exit + ) + return cls - return decorator - return JacFeatureDefaults.make_edge(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl @@ -368,46 +558,48 @@ def make_walker( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a walker architype.""" - if FastAPI.is_enabled(): - - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, - arch_base=WalkerArchitype, - on_entry=on_entry, - on_exit=on_exit, - ) - populate_apis(cls) - return cls + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_walker(on_entry=on_entry, on_exit=on_exit) + + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, + arch_base=WalkerArchitype, + on_entry=on_entry, + on_exit=on_exit, + ) + populate_apis(cls) + return cls - return decorator - return JacFeatureDefaults.make_walker(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl def report(expr: Any) -> None: # noqa:ANN401 """Jac's report stmt feature.""" - if FastAPI.is_enabled(): - JaseciContext.get().reports.append(expr) - return - JacFeatureDefaults.report(expr=expr) + if not FastAPI.is_enabled(): + return JacFeatureImpl.report(expr=expr) + + JaseciContext.get().reports.append(expr) @staticmethod @hookimpl def get_root() -> Root: """Jac's assign comprehension feature.""" - if FastAPI.is_enabled(): - return JaseciContext.get_root() - return JacFeatureDefaults.get_root() # type:ignore[return-value] + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_root() # type:ignore[return-value] + + return JaseciContext.get_root() @staticmethod @hookimpl def get_root_type() -> Type[Root]: """Jac's root getter.""" - if FastAPI.is_enabled(): - return Root - return JacFeatureDefaults.get_root_type() # type:ignore[return-value] + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_root_type() # type:ignore[return-value] + + return Root @staticmethod @hookimpl @@ -417,33 +609,53 @@ def build_edge( conn_assign: tuple[tuple, tuple] | None, ) -> Callable[[NodeAnchor, NodeAnchor], EdgeArchitype]: """Jac's root getter.""" - if FastAPI.is_enabled(): - conn_type = conn_type if conn_type else GenericEdge - - def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: - edge = conn_type() if isinstance(conn_type, type) else conn_type - edge.__attach__(source, target, is_undirected) - if conn_assign: - for fld, val in zip(conn_assign[0], conn_assign[1]): - if hasattr(edge, fld): - setattr(edge, fld, val) - else: - raise ValueError(f"Invalid attribute: {fld}") - if source.persistent or target.persistent: - edge.__jac__.save() - target.save() - source.save() - return edge - - return builder - return JacFeatureDefaults.build_edge( # type:ignore[return-value] - is_undirected=is_undirected, conn_type=conn_type, conn_assign=conn_assign - ) + if not FastAPI.is_enabled(): + return JacFeatureImpl.build_edge( # type:ignore[return-value] + is_undirected=is_undirected, + conn_type=conn_type, + conn_assign=conn_assign, + ) + + conn_type = conn_type if conn_type else GenericEdge + + def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: + edge = conn_type() if isinstance(conn_type, type) else conn_type + + eanch = edge.__jac__ = EdgeAnchor( + architype=edge, + name=("" if isinstance(edge, GenericEdge) else edge.__class__.__name__), + source=source, + target=target, + is_undirected=is_undirected, + access=Permission(), + state=AnchorState(), + ) + source.edges.append(eanch) + target.edges.append(eanch) + source.connect_edge(eanch) + target.connect_edge(eanch) + + if conn_assign: + for fld, val in zip(conn_assign[0], conn_assign[1]): + if hasattr(edge, fld): + setattr(edge, fld, val) + else: + raise ValueError(f"Invalid attribute: {fld}") + if source.persistent or target.persistent: + Jac.save(eanch) + Jac.save(target) + Jac.save(source) + return edge + + return builder @staticmethod @hookimpl def get_object(id: str) -> Architype | None: """Get object via reference id.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_object(id=id) + with suppress(ValueError): if isinstance(architype := BaseAnchor.ref(id).architype, Architype): return architype @@ -454,4 +666,103 @@ def get_object(id: str) -> Architype | None: @hookimpl def object_ref(obj: Architype) -> str: """Get object reference id.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.object_ref(obj=obj) + return str(obj.__jac__.ref_id) + + @staticmethod + @hookimpl + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Invoke data spatial call.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.spawn_call( + op1=op1, op2=op2 + ) # type:ignore[return-value] + + if isinstance(op1, WalkerArchitype): + warch = op1 + walker = op1.__jac__ + if isinstance(op2, NodeArchitype): + node = op2.__jac__ + elif isinstance(op2, EdgeArchitype): + node = op2.__jac__.target + else: + raise TypeError("Invalid target object") + elif isinstance(op2, WalkerArchitype): + warch = op2 + walker = op2.__jac__ + if isinstance(op1, NodeArchitype): + node = op1.__jac__ + elif isinstance(op1, EdgeArchitype): + node = op1.__jac__.target + else: + raise TypeError("Invalid target object") + else: + raise TypeError("Invalid walker object") + + walker.path = [] + walker.next = [node] + walker.returns = [] + while len(walker.next): + if current_node := walker.next.pop(0).architype: + for i in current_node._jac_entry_funcs_: + if not i.trigger or isinstance(walker, i.trigger): + if i.func: + walker.returns.append(i.func(current_node, warch)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_entry_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func: + walker.returns.append(i.func(warch, current_node)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_exit_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func: + walker.returns.append(i.func(warch, current_node)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in current_node._jac_exit_funcs_: + if not i.trigger or isinstance(walker, i.trigger): + if i.func: + walker.returns.append(i.func(current_node, warch)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + walker.ignores = [] + return warch + + @staticmethod + @hookimpl + def destroy(obj: Architype | Anchor | BaseAnchor) -> None: + """Destroy object.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.destroy(obj=obj) # type:ignore[arg-type] + + anchor = obj.__jac__ if isinstance(obj, Architype) else obj + + if ( + isinstance(anchor, BaseAnchor) + and anchor.state.deleted is None + and Jac.check_write_access(anchor) # type: ignore[arg-type] + ): + anchor.state.deleted = False + match anchor: + case NodeAnchor(): + for edge in anchor.edges: + Jac.destroy(edge) + case EdgeAnchor(): + Jac.detach(anchor) + case _: + pass + + Jac.get_context().mem.remove(anchor.id) diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index 886bd096d3..3d32c7c1c4 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -281,7 +281,7 @@ walker manual_detach_nested_node { can enter_root with `root entry { nested = [-->(`?Nested)][0]; detached = here del--> [-->(`?Nested)]; - nested.__jac__.destroy(); + Jac.destroy(nested); nested.__jac__.apply(); # simulate no auto save @@ -296,7 +296,7 @@ walker manual_detach_nested_node { walker delete_nested_node { can enter_root with `root entry { nested = [-->(`?Nested)][0]; - nested.__jac__.destroy(); + Jac.destroy(nested); # nested.__jac__.apply(); report [-->(`?Nested)]; @@ -306,7 +306,7 @@ walker delete_nested_node { walker manual_delete_nested_node { can enter_root with `root entry { nested = [-->(`?Nested)][0]; - nested.__jac__.destroy(); + Jac.destroy(nested); nested.__jac__.apply(); # simulate no auto save @@ -321,7 +321,7 @@ walker manual_delete_nested_node { walker delete_nested_edge { can enter_root with `root entry { nested_edge = :e:[-->][0]; - nested_edge.__jac__.destroy(); + Jac.destroy(nested_edge); report [-->(`?Nested)]; } @@ -330,7 +330,7 @@ walker delete_nested_edge { walker manual_delete_nested_edge { can enter_root with `root entry { nested_edge = :e:[-->][0]; - nested_edge.__jac__.destroy(); + Jac.destroy(nested_edge); nested_edge.__jac__.apply(); # simulate no auto save @@ -347,17 +347,17 @@ walker allow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here, self.level); } else { - here.__jac__.allow_root(BaseAnchor.ref(self.root_id), self.level); + Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); } } can enter_nested with Nested entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here, self.level); } else { - here.__jac__.allow_root(BaseAnchor.ref(self.root_id), self.level); + Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); } } } @@ -367,17 +367,17 @@ walker disallow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here); } else { - here.__jac__.disallow_root(BaseAnchor.ref(self.root_id)); + Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); } } can enter_nested with Nested entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here); } else { - here.__jac__.disallow_root(BaseAnchor.ref(self.root_id)); + Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); } } }