From 55b0a4fa30e408eef1d07978edfaed545b1212a2 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Wed, 16 Oct 2024 04:53:42 +0800 Subject: [PATCH] [FEATURE]: purge_graph - purge whole graph excluding root --- jac-cloud/jac_cloud/core/architype.py | 24 +- jac-cloud/jac_cloud/plugin/jaseci.py | 31 +++ jac-cloud/jac_cloud/plugin/mini/cli_mini.py | 20 +- jac-cloud/jac_cloud/tests/openapi_specs.yaml | 212 ++++++++++++++++++ .../jac_cloud/tests/openapi_specs_mini.yaml | 195 ++++++++++++++++ jac-cloud/jac_cloud/tests/simple_graph.jac | 21 +- .../jac_cloud/tests/simple_graph_mini.jac | 122 ++++++++++ .../jac_cloud/tests/test_simple_graph.py | 52 ++++- .../jac_cloud/tests/test_simple_graph_mini.py | 50 ++++- jac/jaclang/plugin/default.py | 26 ++- jac/jaclang/plugin/feature.py | 5 + jac/jaclang/plugin/spec.py | 6 + .../plugin/tests/fixtures/graph_purger.jac | 101 +++++++++ jac/jaclang/plugin/tests/test_jaseci.py | 69 ++++++ jac/jaclang/runtimelib/memory.py | 2 +- 15 files changed, 896 insertions(+), 40 deletions(-) create mode 100644 jac/jaclang/plugin/tests/fixtures/graph_purger.jac diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index 349b32d47f..7e42adbe82 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -387,15 +387,15 @@ def _pull(self) -> dict: def add_to_set(self, field: str, anchor: Anchor, remove: bool = False) -> None: """Add to set.""" if field not in (add_to_set := self._add_to_set): - add_to_set[field] = {"$each": set()} + add_to_set[field] = {"$each": []} - ops: set = add_to_set[field]["$each"] + ops: list = add_to_set[field]["$each"] if remove: if anchor in ops: ops.remove(anchor) else: - ops.add(anchor) + ops.append(anchor) self.pull(field, anchor, True) def pull(self, field: str, anchor: Anchor, remove: bool = False) -> None: @@ -432,9 +432,10 @@ def make_stub(self: "BaseAnchor | TANCH") -> "BaseAnchor | TANCH": """Return unsynced copy of anchor.""" if self.is_populated(): unloaded = object.__new__(self.__class__) - unloaded.name = self.name - unloaded.id = self.id - return unloaded + # this will be refactored on abstraction + unloaded.name = self.name # type: ignore[attr-defined] + unloaded.id = self.id # type: ignore[attr-defined] + return unloaded # type: ignore[return-value] return self def populate(self) -> None: @@ -535,7 +536,7 @@ def update(self, bulk_write: BulkWrite, propagate: bool = False) -> None: _added_edges = [] for anchor in added_edges: if propagate: - anchor.build_query(bulk_write) + anchor.build_query(bulk_write) # type: ignore[operator] _added_edges.append(anchor.ref_id) changes["$addToSet"]["edges"]["$each"] = _added_edges else: @@ -552,9 +553,10 @@ def update(self, bulk_write: BulkWrite, propagate: bool = False) -> None: if pulled_edges: _pulled_edges = [] for anchor in pulled_edges: - if propagate and anchor.state.deleted is not True: - anchor.state.deleted = True - bulk_write.del_edge(anchor.id) + # will be refactored on abstraction + if propagate and anchor.state.deleted is not True: # type: ignore[attr-defined] + anchor.state.deleted = True # type: ignore[attr-defined] + bulk_write.del_edge(anchor.id) # type: ignore[attr-defined, arg-type] _pulled_edges.append(anchor.ref_id) if added_edges: @@ -643,7 +645,7 @@ class NodeAnchor(BaseAnchor, _NodeAnchor): # type: ignore[misc] """Node Anchor.""" architype: "NodeArchitype" - edges: list["EdgeAnchor"] + edges: list["EdgeAnchor"] # type: ignore[assignment] class Collection(BaseCollection["NodeAnchor"]): """NodeAnchor collection interface.""" diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index 25bc4f8f6c..10734f2fc6 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -501,6 +501,37 @@ def get_context() -> ExecutionContext: return JaseciContext.get() + @staticmethod + @hookimpl + def reset_graph(root: Root | None = None) -> int: + """Purge current or target graph.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.reset_graph(root=root) # type: ignore[arg-type] + + ctx = JaseciContext.get() + ranchor = root.__jac__ if root else ctx.root + + deleted_count = 0 # noqa: SIM113 + + for node in NodeAnchor.Collection.find( + {"_id": {"$ne": ranchor.id}, "root": ranchor.id} + ): + ctx.mem.__mem__[node.id] = node + Jac.destroy(node) + deleted_count += 1 + + for edge in EdgeAnchor.Collection.find({"root": ranchor.id}): + ctx.mem.__mem__[edge.id] = edge + Jac.destroy(edge) + deleted_count += 1 + + for walker in WalkerAnchor.Collection.find({"root": ranchor.id}): + ctx.mem.__mem__[walker.id] = walker + Jac.destroy(walker) + deleted_count += 1 + + return deleted_count + @staticmethod @hookimpl def make_architype( diff --git a/jac-cloud/jac_cloud/plugin/mini/cli_mini.py b/jac-cloud/jac_cloud/plugin/mini/cli_mini.py index e17d5d3658..3d4db6718f 100644 --- a/jac-cloud/jac_cloud/plugin/mini/cli_mini.py +++ b/jac-cloud/jac_cloud/plugin/mini/cli_mini.py @@ -87,14 +87,15 @@ def populate_apis(router: APIRouter, cls: Type[WalkerArchitype]) -> None: files: dict[str, Any] = {} hintings = get_type_hints(cls) - for f in fields(cls): - f_name = f.name - f_type = hintings[f_name] - if f_type in FILE_TYPES: - files[f_name] = gen_model_field(f_type, f, True) - else: - consts = gen_model_field(f_type, f) - body[f_name] = consts + if is_dataclass(cls): + for f in fields(cls): + f_name = f.name + f_type = hintings[f_name] + if f_type in FILE_TYPES: + files[f_name] = gen_model_field(f_type, f, True) + else: + consts = gen_model_field(f_type, f) + body[f_name] = consts payload: dict[str, Any] = { "files": ( @@ -134,6 +135,9 @@ def api_entry( Jac.spawn_call(cls(**body, **pl["files"]), jctx.entry_node.architype) jctx.close() + if jctx.custom is not MISSING: + return jctx.custom + return response(jctx.reports, getattr(jctx, "status", 200)) def api_root( diff --git a/jac-cloud/jac_cloud/tests/openapi_specs.yaml b/jac-cloud/jac_cloud/tests/openapi_specs.yaml index a7fa6a14a1..7881a6fd12 100644 --- a/jac-cloud/jac_cloud/tests/openapi_specs.yaml +++ b/jac-cloud/jac_cloud/tests/openapi_specs.yaml @@ -841,6 +841,59 @@ paths: tags: - walker - walker + /walker/check_populated_graph: + post: + operationId: api_root_walker_check_populated_graph_post + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Root Walker Check Populated Graph Post + description: Successful Response + security: + - HTTPBearer: [] + summary: /check_populated_graph + tags: + - walker + - walker + /walker/check_populated_graph/{node}: + post: + operationId: api_entry_walker_check_populated_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Entry Walker Check Populated Graph Node Post + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: /check_populated_graph/{node} + tags: + - walker + - walker /walker/combination1: get: operationId: api_root_walker_combination1_get @@ -2789,6 +2842,59 @@ paths: tags: - walker - walker + /walker/populate_graph: + post: + operationId: api_root_walker_populate_graph_post + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Root Walker Populate Graph Post + description: Successful Response + security: + - HTTPBearer: [] + summary: /populate_graph + tags: + - walker + - walker + /walker/populate_graph/{node}: + post: + operationId: api_entry_walker_populate_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Entry Walker Populate Graph Node Post + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: /populate_graph/{node} + tags: + - walker + - walker /walker/post_all_excluded: post: operationId: api_root_walker_post_all_excluded_post @@ -3313,6 +3419,59 @@ paths: tags: - walker - walker + /walker/purge_populated_graph: + post: + operationId: api_root_walker_purge_populated_graph_post + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Root Walker Purge Populated Graph Post + description: Successful Response + security: + - HTTPBearer: [] + summary: /purge_populated_graph + tags: + - walker + - walker + /walker/purge_populated_graph/{node}: + post: + operationId: api_entry_walker_purge_populated_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Entry Walker Purge Populated Graph Node Post + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: /purge_populated_graph/{node} + tags: + - walker + - walker /walker/traverse_graph: post: operationId: api_root_walker_traverse_graph_post @@ -3366,6 +3525,59 @@ paths: tags: - walker - walker + /walker/traverse_populated_graph: + post: + operationId: api_root_walker_traverse_populated_graph_post + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Root Walker Traverse Populated Graph Post + description: Successful Response + security: + - HTTPBearer: [] + summary: /traverse_populated_graph + tags: + - walker + - walker + /walker/traverse_populated_graph/{node}: + post: + operationId: api_entry_walker_traverse_populated_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/ContextResponse_NoneType_' + - {} + title: Response Api Entry Walker Traverse Populated Graph Node Post + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + security: + - HTTPBearer: [] + summary: /traverse_populated_graph/{node} + tags: + - walker + - walker /walker/update_graph: post: operationId: api_root_walker_update_graph_post diff --git a/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml b/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml index b42cda0c7b..32a3b998f8 100644 --- a/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml +++ b/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml @@ -338,6 +338,45 @@ paths: summary: /allow_other_root_access/{node} tags: - walker + /walker/check_populated_graph: + post: + operationId: api_root_walker_check_populated_graph_post + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: /check_populated_graph + tags: + - walker + /walker/check_populated_graph/{node}: + post: + operationId: api_entry_walker_check_populated_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: /check_populated_graph/{node} + tags: + - walker /walker/combination1: post: operationId: api_root_walker_combination1_post @@ -530,6 +569,45 @@ paths: summary: /create_nested_node/{node} tags: - walker + /walker/custom_report: + post: + operationId: api_root_walker_custom_report_post + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: /custom_report + tags: + - walker + /walker/custom_report/{node}: + post: + operationId: api_entry_walker_custom_report__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: /custom_report/{node} + tags: + - walker /walker/custom_status_code: post: operationId: api_root_walker_custom_status_code_post @@ -992,6 +1070,45 @@ paths: summary: /get_with_query/{node} tags: - walker + /walker/populate_graph: + post: + operationId: api_root_walker_populate_graph_post + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: /populate_graph + tags: + - walker + /walker/populate_graph/{node}: + post: + operationId: api_entry_walker_populate_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: /populate_graph/{node} + tags: + - walker /walker/post_all_excluded: post: operationId: api_root_walker_post_all_excluded_post @@ -1373,6 +1490,45 @@ paths: summary: /post_with_file/{node} tags: - walker + /walker/purge_populated_graph: + post: + operationId: api_root_walker_purge_populated_graph_post + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: /purge_populated_graph + tags: + - walker + /walker/purge_populated_graph/{node}: + post: + operationId: api_entry_walker_purge_populated_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: /purge_populated_graph/{node} + tags: + - walker /walker/traverse_graph: post: operationId: api_root_walker_traverse_graph_post @@ -1412,6 +1568,45 @@ paths: summary: /traverse_graph/{node} tags: - walker + /walker/traverse_populated_graph: + post: + operationId: api_root_walker_traverse_populated_graph_post + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + summary: /traverse_populated_graph + tags: + - walker + /walker/traverse_populated_graph/{node}: + post: + operationId: api_entry_walker_traverse_populated_graph__node__post + parameters: + - in: path + name: node + required: true + schema: + anyOf: + - type: string + - type: 'null' + title: Node + responses: + '200': + content: + application/json: + schema: {} + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: /traverse_populated_graph/{node} + tags: + - walker /walker/update_graph: post: operationId: api_root_walker_update_graph_post diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index 3fcc4753b9..e8baaae4fa 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -263,18 +263,13 @@ walker manual_delete_nested_edge { } } -walker custom_report { - can enter1 with `root entry { - report 1; - report 2; - report 3; - - Jac.report({ - "testing": 1 - }, True); - } +:walker:check_populated_graph:can:enter { + import:py from jac_cloud.core.architype {NodeAnchor, EdgeAnchor, WalkerAnchor} - class __specs__ { - has auth: bool = False; - } + id = here.__jac__.id; + count = NodeAnchor.Collection.count({"$or": [{"_id": id}, {"root": id}]}); + count += EdgeAnchor.Collection.count({"root": id}); + count += WalkerAnchor.Collection.count({"root": id}); + + report count; } \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/simple_graph_mini.jac b/jac-cloud/jac_cloud/tests/simple_graph_mini.jac index 70d5a5cb4a..4ebe304453 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph_mini.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph_mini.jac @@ -415,4 +415,126 @@ walker different_return { class __specs__ { has auth: bool = False; } +} + +walker custom_report { + can enter1 with `root entry { + report 1; + report 2; + report 3; + + Jac.report({ + "testing": 1 + }, True); + } + + class __specs__ { + has auth: bool = False; + } +} + +################################################################## +# FOR PURGER # +################################################################## + +node D { + has id: int; +} + +node E { + has id: int; +} + +node F { + has id: int; +} + +node G { + has id: int; +} + +node H { + has id: int; +} + + +walker populate_graph { + can setup1 with `root entry { + for i in range(2) { + here ++> D(id=i); + } + visit [-->]; + } + + can setup2 with D entry { + for i in range(2) { + here ++> E(id=i); + } + visit [-->]; + } + + can setup3 with E entry { + for i in range(2) { + here ++> F(id=i); + } + visit [-->]; + } + + can setup4 with F entry { + for i in range(2) { + here ++> G(id=i); + } + visit [-->]; + } + + can setup5 with G entry { + for i in range(2) { + here ++> H(id=i); + } + visit [-->]; + } +} + +walker traverse_populated_graph { + can enter1 with `root entry { + report here; + visit [-->]; + } + + can enter2 with D entry { + report here; + visit [-->]; + } + + can enter3 with E entry { + report here; + visit [-->]; + } + + can enter4 with F entry { + report here; + visit [-->]; + } + + can enter5 with G entry { + report here; + visit [-->]; + } + + can enter6 with H entry { + report here; + visit [-->]; + } +} + +walker purge_populated_graph { + can purge with `root entry { + report Jac.reset_graph(); + } +} + +walker check_populated_graph { + can enter with `root entry { + report len(Jac.get_context().mem.__shelf__.values()); + } } \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph.py b/jac-cloud/jac_cloud/tests/test_simple_graph.py index 5cc1b82395..e848f77161 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph.py @@ -612,19 +612,19 @@ def trigger_upload_file(self) -> None: "single": { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7113, } }, "multiple": [ { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7113, }, { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7113, }, ], "singleOptional": None, @@ -633,12 +633,52 @@ def trigger_upload_file(self) -> None: data["reports"], ) + def trigger_reset_graph(self) -> None: + """Test custom status code.""" + res = self.post_api("populate_graph", user=2) + self.assertEqual(200, res["status"]) + self.assertEqual([None] * 31, res["returns"]) + + res = self.post_api("traverse_populated_graph", user=2) + self.assertEqual(200, res["status"]) + self.assertEqual([None] * 63, res["returns"]) + reports = res["reports"] + + root = reports.pop(0) + self.assertTrue(root["id"].startswith("n::")) + self.assertEqual({}, root["context"]) + + cur = 0 + max = 2 + for node in ["D", "E", "F", "G", "H"]: + for idx in range(cur, cur + max): + self.assertTrue(reports[idx]["id"].startswith(f"n:{node}:")) + self.assertEqual({"id": idx % 2}, reports[idx]["context"]) + cur += 1 + max = max * 2 + + res = self.post_api("check_populated_graph", user=2) + self.assertEqual(200, res["status"]) + self.assertEqual([None], res["returns"]) + self.assertEqual([125], res["reports"]) + + res = self.post_api("purge_populated_graph", user=2) + self.assertEqual(200, res["status"]) + self.assertEqual([None], res["returns"]) + self.assertEqual([124], res["reports"]) + + res = self.post_api("check_populated_graph", user=2) + self.assertEqual(200, res["status"]) + self.assertEqual([None], res["returns"]) + self.assertEqual([1], res["reports"]) + async def test_all_features(self) -> None: """Test Full Features.""" self.trigger_openapi_specs_test() self.trigger_create_user_test() self.trigger_create_user_test(suffix="2") + self.trigger_create_user_test(suffix="3") self.trigger_create_graph_test() self.trigger_traverse_graph_test() @@ -722,3 +762,9 @@ async def test_all_features(self) -> None: ################################################### self.trigger_upload_file() + + ################################################### + # TEST PURGER # + ################################################### + + self.trigger_reset_graph() diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py index 9c24a23416..42d74f88b0 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py @@ -370,6 +370,11 @@ def trigger_custom_status_code(self) -> None: Exception, self.post_api, "custom_status_code", {"status": invalid_code} ) + def trigger_custom_report(self) -> None: + """Test custom status code.""" + res = self.post_api("custom_report") + self.assertEqual({"testing": 1}, res) + def trigger_upload_file(self) -> None: """Test upload file.""" with open("jac_cloud/tests/simple_graph.jac", mode="br") as s: @@ -390,19 +395,19 @@ def trigger_upload_file(self) -> None: "single": { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7113, } }, "multiple": [ { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7113, }, { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 6992, + "size": 7113, }, ], "singleOptional": None, @@ -411,6 +416,33 @@ def trigger_upload_file(self) -> None: data["reports"], ) + def trigger_reset_graph(self) -> None: + """Test custom status code.""" + res = self.post_api("populate_graph") + self.assertEqual(200, res["status"]) + + res = self.post_api("traverse_populated_graph") + self.assertEqual(200, res["status"]) + reports = res["reports"] + + root = reports.pop(0) + self.assertEqual({}, root["context"]) + + for idx in range(0, 62): + self.assertEqual({"id": idx % 2}, reports[idx]["context"]) + + res = self.post_api("check_populated_graph") + self.assertEqual(200, res["status"]) + self.assertEqual([125], res["reports"]) + + res = self.post_api("purge_populated_graph") + self.assertEqual(200, res["status"]) + self.assertEqual([124], res["reports"]) + + res = self.post_api("check_populated_graph") + self.assertEqual(200, res["status"]) + self.assertEqual([1], res["reports"]) + async def test_all_features(self) -> None: """Test Full Features.""" self.trigger_openapi_specs_test() @@ -457,8 +489,20 @@ async def test_all_features(self) -> None: self.trigger_custom_status_code() + ################################################### + # CUSTOM REPORT # + ################################################### + + self.trigger_custom_report() + ################################################### # FILE UPLOAD # ################################################### self.trigger_upload_file() + + ################################################### + # TEST PURGER # + ################################################### + + self.trigger_reset_graph() diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index eb58e94a01..1154766b87 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -11,7 +11,7 @@ from dataclasses import field from functools import wraps from logging import getLogger -from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union +from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union, cast from uuid import UUID from jaclang.compiler.constant import colors @@ -41,6 +41,7 @@ ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter from jaclang.runtimelib.machine import JacMachine, JacProgram +from jaclang.runtimelib.memory import Shelf, ShelfStorage from jaclang.runtimelib.utils import collect_node_connections, traverse_graph @@ -566,6 +567,29 @@ def get_context() -> ExecutionContext: """Get current execution context.""" return ExecutionContext.get() + @staticmethod + @hookimpl + def reset_graph(root: Optional[Root] = None) -> int: + """Purge current or target graph.""" + ctx = Jac.get_context() + mem = cast(ShelfStorage, ctx.mem) + ranchor = root.__jac__ if root else ctx.root + + deleted_count = 0 + for anchor in ( + anchors.values() + if isinstance(anchors := mem.__shelf__, Shelf) + else mem.__mem__.values() + ): + if anchor == ranchor or anchor.root != ranchor.id: + continue + + if loaded_anchor := mem.find_by_id(anchor.id): + deleted_count += 1 + Jac.destroy(loaded_anchor) + + return deleted_count + @staticmethod @hookimpl def get_object(id: str) -> Architype | None: diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index b88c4edfc7..fffa03b55e 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -254,6 +254,11 @@ def get_context() -> ExecutionContext: """Get current execution context.""" return plugin_manager.hook.get_context() + @staticmethod + def reset_graph(root: Optional[Root] = None) -> int: + """Purge current or target graph.""" + return plugin_manager.hook.reset_graph(root=root) + @staticmethod def get_object(id: str) -> Architype | None: """Get object given id.""" diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index fc455b8237..da3fb37646 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -244,6 +244,12 @@ def get_context() -> ExecutionContext: """Get current execution context.""" raise NotImplementedError + @staticmethod + @hookspec(firstresult=True) + def reset_graph(root: Optional[Root]) -> int: + """Purge current or target graph.""" + raise NotImplementedError + @staticmethod @hookspec(firstresult=True) def get_object(id: str) -> Architype | None: diff --git a/jac/jaclang/plugin/tests/fixtures/graph_purger.jac b/jac/jaclang/plugin/tests/fixtures/graph_purger.jac new file mode 100644 index 0000000000..3044008aa4 --- /dev/null +++ b/jac/jaclang/plugin/tests/fixtures/graph_purger.jac @@ -0,0 +1,101 @@ +node A { + has id: int; +} + +node B { + has id: int; +} + +node C { + has id: int; +} + +node D { + has id: int; +} + +node E { + has id: int; +} + + +walker populate { + can setup1 with `root entry { + for i in range(2) { + here ++> A(id=i); + } + visit [-->]; + } + + can setup2 with A entry { + for i in range(2) { + here ++> B(id=i); + } + visit [-->]; + } + + can setup3 with B entry { + for i in range(2) { + here ++> C(id=i); + } + visit [-->]; + } + + can setup4 with C entry { + for i in range(2) { + here ++> D(id=i); + } + visit [-->]; + } + + can setup5 with D entry { + for i in range(2) { + here ++> E(id=i); + } + visit [-->]; + } +} + +walker traverse { + can enter1 with `root entry { + print(here); + visit [-->]; + } + + can enter2 with A entry { + print(here); + visit [-->]; + } + + can enter3 with B entry { + print(here); + visit [-->]; + } + + can enter4 with C entry { + print(here); + visit [-->]; + } + + can enter5 with D entry { + print(here); + visit [-->]; + } + + can enter6 with E entry { + print(here); + visit [-->]; + } +} + +walker purge { + can purge with `root entry { + print(Jac.reset_graph()); + } +} + +walker check { + can enter with `root entry { + print(len(Jac.get_context().mem.__shelf__.values())); + } +} \ No newline at end of file diff --git a/jac/jaclang/plugin/tests/test_jaseci.py b/jac/jaclang/plugin/tests/test_jaseci.py index af7e34bae1..c60bb0e2e1 100644 --- a/jac/jaclang/plugin/tests/test_jaseci.py +++ b/jac/jaclang/plugin/tests/test_jaseci.py @@ -269,6 +269,75 @@ def test_indirect_reference_node(self) -> None: ) self._del_session(session) + def test_walker_purger(self) -> None: + """Test simple persistent object.""" + session = self.fixture_abs_path("test_walker_purger.session") + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="populate", + args=[], + ) + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="traverse", + args=[], + ) + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="check", + args=[], + ) + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="purge", + args=[], + ) + output = self.capturedOutput.getvalue().strip() + self.assertEqual( + output, + ( + "Root()\n" + "A(id=0)\nA(id=1)\n" + "B(id=0)\nB(id=1)\nB(id=0)\nB(id=1)\n" + "C(id=0)\nC(id=1)\nC(id=0)\nC(id=1)\nC(id=0)\nC(id=1)\nC(id=0)\nC(id=1)\n" + "D(id=0)\nD(id=1)\nD(id=0)\nD(id=1)\nD(id=0)\nD(id=1)\nD(id=0)\nD(id=1)\n" + "D(id=0)\nD(id=1)\nD(id=0)\nD(id=1)\nD(id=0)\nD(id=1)\nD(id=0)\nD(id=1)\n" + "E(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\n" + "E(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\n" + "E(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\n" + "E(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\nE(id=0)\nE(id=1)\n" + "125\n124" + ), + ) + self._output2buffer() + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="traverse", + args=[], + ) + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="check", + args=[], + ) + cli.enter( + filename=self.fixture_abs_path("graph_purger.jac"), + session=session, + entrypoint="purge", + args=[], + ) + output = self.capturedOutput.getvalue().strip() + self.assertEqual(output, "Root()\n1\n0") + + self._del_session(session) + def trigger_access_validation_test( self, give_access_to_full_graph: bool, via_all: bool = False ) -> None: diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index 8c5085e28e..5cd5b88525 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -100,7 +100,7 @@ def close(self) -> None: and p_d.edges != d.edges and Jac.check_connect_access(d) ): - if not d.edges: + if not d.edges and not isinstance(d.architype, Root): self.__shelf__.pop(_id, None) continue p_d.edges = d.edges