From 25f96561cb05d701a7a48ff670aa1fe3b135941d Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Tue, 8 Oct 2024 03:34:48 +0800 Subject: [PATCH] [JAC-CLOUD-MIN]: Initial Mini Version --- .github/workflows/test-cloud.yml | 5 + jac-cloud/jac_cloud/plugin/cli.py | 15 +- jac-cloud/jac_cloud/plugin/jaseci.py | 5 +- jac-cloud/jac_cloud/plugin/mini/__init__.py | 1 + jac-cloud/jac_cloud/plugin/mini/cli_mini.py | 188 ++ .../jac_cloud/tests/openapi_specs_mini.json | 2075 +++++++++++++++++ .../jac_cloud/tests/simple_graph_mini.jac | 347 +++ .../jac_cloud/tests/test_simple_graph_mini.py | 416 ++++ 8 files changed, 3044 insertions(+), 8 deletions(-) create mode 100644 jac-cloud/jac_cloud/plugin/mini/__init__.py create mode 100644 jac-cloud/jac_cloud/plugin/mini/cli_mini.py create mode 100644 jac-cloud/jac_cloud/tests/openapi_specs_mini.json create mode 100644 jac-cloud/jac_cloud/tests/simple_graph_mini.jac create mode 100644 jac-cloud/jac_cloud/tests/test_simple_graph_mini.py diff --git a/.github/workflows/test-cloud.yml b/.github/workflows/test-cloud.yml index d4a862c164..38e65879f5 100644 --- a/.github/workflows/test-cloud.yml +++ b/.github/workflows/test-cloud.yml @@ -56,6 +56,11 @@ jobs: run: | jac clean jac tool gen_parser + + jac serve jac_cloud/tests/simple_graph_mini.jac --port 8001 & + + export DATABASE_HOST="mongodb://localhost/?retryWrites=true&w=majority" jac serve jac_cloud/tests/simple_graph.jac --port 8000 & + sleep 3 pytest diff --git a/jac-cloud/jac_cloud/plugin/cli.py b/jac-cloud/jac_cloud/plugin/cli.py index 54a46abfb1..4b3c82050c 100644 --- a/jac-cloud/jac_cloud/plugin/cli.py +++ b/jac-cloud/jac_cloud/plugin/cli.py @@ -1,7 +1,8 @@ """Module for registering CLI plugins for jaseci.""" -import os -import pickle +from os import getenv +from os.path import split +from pickle import load from jaclang import jac_import from jaclang.cli.cmdreg import cmd_registry @@ -9,6 +10,8 @@ from jaclang.runtimelib.context import ExecutionContext from jaclang.runtimelib.machine import JacMachine, JacProgram +from .mini.cli_mini import serve_mini + class JacCmd: """Jac CLI.""" @@ -20,10 +23,14 @@ def create_cmd() -> None: @cmd_registry.register def serve(filename: str, host: str = "0.0.0.0", port: int = 8000) -> None: + if not getenv("DATABASE_HOST"): + serve_mini(filename=filename, host=host, port=port) + return + from jac_cloud import FastAPI """Serve the jac application.""" - base, mod = os.path.split(filename) + base, mod = split(filename) base = base if base else "./" mod = mod[:-4] @@ -40,7 +47,7 @@ def serve(filename: str, host: str = "0.0.0.0", port: int = 8000) -> None: elif filename.endswith(".jir"): with open(filename, "rb") as f: JacMachine(base).attach_program( - JacProgram(mod_bundle=pickle.load(f), bytecode=None) + JacProgram(mod_bundle=load(f), bytecode=None) ) jac_import( target=mod, diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index acc141d9c3..6aa146e81c 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -592,10 +592,7 @@ def decorator(cls: Type[Architype]) -> Type[Architype]: @hookimpl def report(expr: Any) -> None: # noqa:ANN401 """Jac's report stmt feature.""" - if not FastAPI.is_enabled(): - return JacFeatureImpl.report(expr=expr) - - JaseciContext.get().reports.append(expr) + Jac.get_context().reports.append(expr) @staticmethod @hookimpl diff --git a/jac-cloud/jac_cloud/plugin/mini/__init__.py b/jac-cloud/jac_cloud/plugin/mini/__init__.py new file mode 100644 index 0000000000..1fa8f770d8 --- /dev/null +++ b/jac-cloud/jac_cloud/plugin/mini/__init__.py @@ -0,0 +1 @@ +"""Jaseci Plugins Mini.""" diff --git a/jac-cloud/jac_cloud/plugin/mini/cli_mini.py b/jac-cloud/jac_cloud/plugin/mini/cli_mini.py new file mode 100644 index 0000000000..e41b7ea4a6 --- /dev/null +++ b/jac-cloud/jac_cloud/plugin/mini/cli_mini.py @@ -0,0 +1,188 @@ +"""Module for registering CLI plugins for jaseci.""" + +from dataclasses import Field, MISSING, asdict, fields, is_dataclass +from inspect import isclass +from os import getenv, path +from pickle import load +from typing import Any, Type, cast, get_type_hints + +from asyncer import syncify + +from fastapi import Depends, FastAPI, File, Response, UploadFile, status +from fastapi.responses import ORJSONResponse + +from jaclang import jac_import +from jaclang.runtimelib.architype import ( + Anchor, + Architype, + WalkerAnchor, + WalkerArchitype, +) +from jaclang.runtimelib.context import ExecutionContext +from jaclang.runtimelib.machine import JacMachine, JacProgram + +from orjson import loads + +from pydantic import BaseModel, Field as pyField, ValidationError, create_model + +from starlette.datastructures import UploadFile as BaseUploadFile + +from uvicorn import run + +FILE_TYPES = { + UploadFile, + list[UploadFile], + UploadFile | None, + list[UploadFile] | None, +} + + +def response(reports: list[Any], status: int = 200) -> ORJSONResponse: + """Return serialized version of reports.""" + resp: dict[str, Any] = {"status": status} + + for key, val in enumerate(reports): + clean_response(key, val, reports) + resp["reports"] = reports + + return ORJSONResponse(resp, status_code=status) + + +def clean_response(key: str | int, val: Any, obj: list | dict) -> None: # noqa: ANN401 + """Cleanup and override current object.""" + match val: + case list(): + for idx, lval in enumerate(val): + clean_response(idx, lval, val) + case dict(): + for key, dval in val.items(): + clean_response(key, dval, val) + case Anchor(): + cast(dict, obj)[key] = asdict(val.report()) + case Architype(): + cast(dict, obj)[key] = asdict(val.__jac__.report()) + case val if is_dataclass(val) and not isinstance(val, type): + cast(dict, obj)[key] = asdict(val) + case _: + pass + + +def gen_model_field(cls: type, field: Field, is_file: bool = False) -> tuple[type, Any]: + """Generate Specs for Model Field.""" + if field.default is not MISSING: + consts = (cls, pyField(default=field.default)) + elif callable(field.default_factory): + consts = (cls, pyField(default_factory=field.default_factory)) + else: + consts = (cls, File(...) if is_file else ...) + + return consts + + +def populate_apis(app: FastAPI, cls: Type[WalkerArchitype]) -> None: + """Generate FastAPI endpoint based on WalkerArchitype class.""" + body: dict[str, Any] = {} + 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 + + payload: dict[str, Any] = { + "files": ( + create_model(f"{cls.__name__.lower()}_files_model", **files), + Depends(), + ), + } + + body_model = None + if body: + body_model = create_model(f"{cls.__name__.lower()}_body_model", **body) + + if files: + payload["body"] = (UploadFile, File(...)) + else: + payload["body"] = (body_model, ...) + + payload_model = create_model(f"{cls.__name__.lower()}_request_model", **payload) + + def api_entry( + node: str | None, + payload: payload_model = Depends(), # type: ignore # noqa: B008 + ) -> ORJSONResponse: + pl = cast(BaseModel, payload).model_dump() + body = pl.get("body", {}) + + if isinstance(body, BaseUploadFile) and body_model: + body = loads(syncify(body.read)()) + try: + body = body_model(**body).model_dump() + except ValidationError as e: + return ORJSONResponse({"detail": e.errors()}) + + jctx = ExecutionContext.create(session=getenv("DATABASE", "database")) + jctx.set_entry_node(node) + + wlk: WalkerAnchor = cls(**body, **pl["files"]).__jac__ + wlk.spawn_call(jctx.entry_node) + jctx.close() + + return response(jctx.reports, getattr(jctx, "status", 200)) + + def api_root( + payload: payload_model = Depends(), # type: ignore # noqa: B008 + ) -> Response: + return api_entry(None, payload) + + app.post(url := f"/{cls.__name__}", summary=url)(api_root) + app.post(url := f"/{cls.__name__}/{{node}}", summary=url)(api_entry) + + +def serve_mini(filename: str, host: str = "0.0.0.0", port: int = 8000) -> None: + """Serve the jac application.""" + base, mod = path.split(filename) + base = base if base else "./" + mod = mod[:-4] + + if filename.endswith(".jac"): + (module,) = jac_import( + target=mod, + base_path=base, + cachable=True, + override_name="__main__", + ) + elif filename.endswith(".jir"): + with open(filename, "rb") as f: + JacMachine(base).attach_program( + JacProgram(mod_bundle=load(f), bytecode=None) + ) + (module,) = jac_import( + target=mod, + base_path=base, + cachable=True, + override_name="__main__", + ) + else: + JacMachine.detach() + raise ValueError("Not a valid file!\nOnly supports `.jac` and `.jir`") + + app = FastAPI() + + @app.get("/", status_code=status.HTTP_200_OK) + def healthz() -> Response: + """Healthz API.""" + return Response() + + for obj in module.__dict__.values(): + if isclass(obj) and issubclass(obj, WalkerArchitype): + populate_apis(app, obj) + + run(app, host=host, port=port) + + JacMachine.detach() diff --git a/jac-cloud/jac_cloud/tests/openapi_specs_mini.json b/jac-cloud/jac_cloud/tests/openapi_specs_mini.json new file mode 100644 index 0000000000..341ba4082b --- /dev/null +++ b/jac-cloud/jac_cloud/tests/openapi_specs_mini.json @@ -0,0 +1,2075 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/": { + "get": { + "summary": "Healthz", + "description": "Healthz API.", + "operationId": "healthz__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/create_graph": { + "post": { + "summary": "/create_graph", + "operationId": "api_root_create_graph_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/create_graph/{node}": { + "post": { + "summary": "/create_graph/{node}", + "operationId": "api_entry_create_graph__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/traverse_graph": { + "post": { + "summary": "/traverse_graph", + "operationId": "api_root_traverse_graph_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/traverse_graph/{node}": { + "post": { + "summary": "/traverse_graph/{node}", + "operationId": "api_entry_traverse_graph__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/detach_node": { + "post": { + "summary": "/detach_node", + "operationId": "api_root_detach_node_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/detach_node/{node}": { + "post": { + "summary": "/detach_node/{node}", + "operationId": "api_entry_detach_node__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/update_graph": { + "post": { + "summary": "/update_graph", + "operationId": "api_root_update_graph_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/update_graph/{node}": { + "post": { + "summary": "/update_graph/{node}", + "operationId": "api_entry_update_graph__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/create_nested_node": { + "post": { + "summary": "/create_nested_node", + "operationId": "api_root_create_nested_node_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/create_nested_node/{node}": { + "post": { + "summary": "/create_nested_node/{node}", + "operationId": "api_entry_create_nested_node__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/update_nested_node": { + "post": { + "summary": "/update_nested_node", + "operationId": "api_root_update_nested_node_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/update_nested_node/{node}": { + "post": { + "summary": "/update_nested_node/{node}", + "operationId": "api_entry_update_nested_node__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/detach_nested_node": { + "post": { + "summary": "/detach_nested_node", + "operationId": "api_root_detach_nested_node_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/detach_nested_node/{node}": { + "post": { + "summary": "/detach_nested_node/{node}", + "operationId": "api_entry_detach_nested_node__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/visit_nested_node": { + "post": { + "summary": "/visit_nested_node", + "operationId": "api_root_visit_nested_node_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/visit_nested_node/{node}": { + "post": { + "summary": "/visit_nested_node/{node}", + "operationId": "api_entry_visit_nested_node__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/delete_nested_node": { + "post": { + "summary": "/delete_nested_node", + "operationId": "api_root_delete_nested_node_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/delete_nested_node/{node}": { + "post": { + "summary": "/delete_nested_node/{node}", + "operationId": "api_entry_delete_nested_node__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/delete_nested_edge": { + "post": { + "summary": "/delete_nested_edge", + "operationId": "api_root_delete_nested_edge_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/delete_nested_edge/{node}": { + "post": { + "summary": "/delete_nested_edge/{node}", + "operationId": "api_entry_delete_nested_edge__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/allow_other_root_access": { + "post": { + "summary": "/allow_other_root_access", + "operationId": "api_root_allow_other_root_access_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/allow_other_root_access_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/allow_other_root_access/{node}": { + "post": { + "summary": "/allow_other_root_access/{node}", + "operationId": "api_entry_allow_other_root_access__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/allow_other_root_access_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/disallow_other_root_access": { + "post": { + "summary": "/disallow_other_root_access", + "operationId": "api_root_disallow_other_root_access_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/disallow_other_root_access_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/disallow_other_root_access/{node}": { + "post": { + "summary": "/disallow_other_root_access/{node}", + "operationId": "api_entry_disallow_other_root_access__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/disallow_other_root_access_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_no_body": { + "post": { + "summary": "/post_no_body", + "operationId": "api_root_post_no_body_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/post_no_body/{node}": { + "post": { + "summary": "/post_no_body/{node}", + "operationId": "api_entry_post_no_body__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_with_body": { + "post": { + "summary": "/post_with_body", + "operationId": "api_root_post_with_body_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/post_with_body_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_with_body/{node}": { + "post": { + "summary": "/post_with_body/{node}", + "operationId": "api_entry_post_with_body__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/post_with_body_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/get_no_body": { + "post": { + "summary": "/get_no_body", + "operationId": "api_root_get_no_body_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/get_no_body/{node}": { + "post": { + "summary": "/get_no_body/{node}", + "operationId": "api_entry_get_no_body__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/get_with_query": { + "post": { + "summary": "/get_with_query", + "operationId": "api_root_get_with_query_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_with_query_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/get_with_query/{node}": { + "post": { + "summary": "/get_with_query/{node}", + "operationId": "api_entry_get_with_query__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_with_query_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/get_all_query": { + "post": { + "summary": "/get_all_query", + "operationId": "api_root_get_all_query_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_all_query_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/get_all_query/{node}": { + "post": { + "summary": "/get_all_query/{node}", + "operationId": "api_entry_get_all_query__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/get_all_query_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_path_var": { + "post": { + "summary": "/post_path_var", + "operationId": "api_root_post_path_var_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/post_path_var_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_path_var/{node}": { + "post": { + "summary": "/post_path_var/{node}", + "operationId": "api_entry_post_path_var__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/post_path_var_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/combination1": { + "post": { + "summary": "/combination1", + "operationId": "api_root_combination1_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/combination1_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/combination1/{node}": { + "post": { + "summary": "/combination1/{node}", + "operationId": "api_entry_combination1__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/combination1_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/combination2": { + "post": { + "summary": "/combination2", + "operationId": "api_root_combination2_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/combination2_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/combination2/{node}": { + "post": { + "summary": "/combination2/{node}", + "operationId": "api_entry_combination2__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/combination2_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_with_file": { + "post": { + "summary": "/post_with_file", + "operationId": "api_root_post_with_file_post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_api_root_post_with_file_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_with_file/{node}": { + "post": { + "summary": "/post_with_file/{node}", + "operationId": "api_entry_post_with_file__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_api_entry_post_with_file__node__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_with_body_and_file": { + "post": { + "summary": "/post_with_body_and_file", + "operationId": "api_root_post_with_body_and_file_post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_api_root_post_with_body_and_file_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/post_with_body_and_file/{node}": { + "post": { + "summary": "/post_with_body_and_file/{node}", + "operationId": "api_entry_post_with_body_and_file__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_api_entry_post_with_body_and_file__node__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/custom_status_code": { + "post": { + "summary": "/custom_status_code", + "operationId": "api_root_custom_status_code_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/custom_status_code_body_model" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/custom_status_code/{node}": { + "post": { + "summary": "/custom_status_code/{node}", + "operationId": "api_entry_custom_status_code__node__post", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Node" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/custom_status_code_body_model" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Body_api_entry_post_with_body_and_file__node__post": { + "properties": { + "body": { + "type": "string", + "format": "binary", + "title": "Body" + }, + "single": { + "type": "string", + "format": "binary", + "title": "Single" + }, + "multiple": { + "items": { + "type": "string", + "format": "binary" + }, + "type": "array", + "title": "Multiple" + } + }, + "type": "object", + "required": [ + "body", + "single", + "multiple" + ], + "title": "Body_api_entry_post_with_body_and_file__node__post" + }, + "Body_api_entry_post_with_file__node__post": { + "properties": { + "single": { + "type": "string", + "format": "binary", + "title": "Single" + }, + "multiple": { + "items": { + "type": "string", + "format": "binary" + }, + "type": "array", + "title": "Multiple" + }, + "singleOptional": { + "anyOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "null" + } + ], + "title": "Singleoptional" + } + }, + "type": "object", + "required": [ + "single", + "multiple" + ], + "title": "Body_api_entry_post_with_file__node__post" + }, + "Body_api_root_post_with_body_and_file_post": { + "properties": { + "body": { + "type": "string", + "format": "binary", + "title": "Body" + }, + "single": { + "type": "string", + "format": "binary", + "title": "Single" + }, + "multiple": { + "items": { + "type": "string", + "format": "binary" + }, + "type": "array", + "title": "Multiple" + } + }, + "type": "object", + "required": [ + "body", + "single", + "multiple" + ], + "title": "Body_api_root_post_with_body_and_file_post" + }, + "Body_api_root_post_with_file_post": { + "properties": { + "single": { + "type": "string", + "format": "binary", + "title": "Single" + }, + "multiple": { + "items": { + "type": "string", + "format": "binary" + }, + "type": "array", + "title": "Multiple" + }, + "singleOptional": { + "anyOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "null" + } + ], + "title": "Singleoptional" + } + }, + "type": "object", + "required": [ + "single", + "multiple" + ], + "title": "Body_api_root_post_with_file_post" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "allow_other_root_access_body_model": { + "properties": { + "root_id": { + "type": "string", + "title": "Root Id" + }, + "level": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "title": "Level" + }, + "via_all": { + "type": "boolean", + "title": "Via All" + } + }, + "type": "object", + "required": [ + "root_id" + ], + "title": "allow_other_root_access_body_model" + }, + "combination1_body_model": { + "properties": { + "a": { + "type": "string", + "title": "A" + }, + "b": { + "type": "string", + "title": "B" + }, + "c": { + "type": "string", + "title": "C" + } + }, + "type": "object", + "required": [ + "a", + "b", + "c" + ], + "title": "combination1_body_model" + }, + "combination2_body_model": { + "properties": { + "a": { + "type": "string", + "title": "A" + }, + "b": { + "type": "string", + "title": "B" + }, + "c": { + "type": "string", + "title": "C" + } + }, + "type": "object", + "required": [ + "a", + "b", + "c" + ], + "title": "combination2_body_model" + }, + "custom_status_code_body_model": { + "properties": { + "status": { + "type": "integer", + "title": "Status" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "custom_status_code_body_model" + }, + "disallow_other_root_access_body_model": { + "properties": { + "root_id": { + "type": "string", + "title": "Root Id" + }, + "via_all": { + "type": "boolean", + "title": "Via All" + } + }, + "type": "object", + "required": [ + "root_id" + ], + "title": "disallow_other_root_access_body_model" + }, + "get_all_query_body_model": { + "properties": { + "a": { + "type": "string", + "title": "A" + }, + "b": { + "type": "string", + "title": "B" + } + }, + "type": "object", + "required": [ + "a", + "b" + ], + "title": "get_all_query_body_model" + }, + "get_with_query_body_model": { + "properties": { + "a": { + "type": "string", + "title": "A" + } + }, + "type": "object", + "required": [ + "a" + ], + "title": "get_with_query_body_model" + }, + "post_path_var_body_model": { + "properties": { + "a": { + "type": "string", + "title": "A" + } + }, + "type": "object", + "required": [ + "a" + ], + "title": "post_path_var_body_model" + }, + "post_with_body_body_model": { + "properties": { + "a": { + "type": "string", + "title": "A" + } + }, + "type": "object", + "required": [ + "a" + ], + "title": "post_with_body_body_model" + } + } + } +} \ 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 new file mode 100644 index 0000000000..8f8f062afb --- /dev/null +++ b/jac-cloud/jac_cloud/tests/simple_graph_mini.jac @@ -0,0 +1,347 @@ +"""Example of simple walker walking nodes.""" +import:py from fastapi {UploadFile} +import:py from uuid {UUID} + +enum Enum { + A = "a", + B = "b", + C = "c" +} + +node A { + has val: int; +} + +node B { + has val: int; +} + +node C { + has val: int; +} + +obj Child { + has val: int, arr: list, json: dict, enum_field: Enum; +} + +obj Parent:Child: { + has child: Child; +} + +node Nested { + has val: int, arr: list, json: dict, parent: Parent, enum_field: Enum; +} + +walker create_graph { + can enter_root with `root entry { + a = A(val=0); + b = B(val=1); + c = C(val=2); + here ++> a; + a ++> b; + b ++> c; + + report here; + report a; + report b; + report c; + } +} + +walker traverse_graph { + can enter with `root entry { + report here; + visit [-->]; + } + + can enter_A with A entry { + report here; + visit [-->]; + } + + can enter_B with B entry { + report here; + visit [-->]; + } + + can enter_C with C entry { + report here; + } +} + +walker detach_node { + can enter with `root entry { + visit [-->]; + } + + can enter_A with A entry { + visit [-->]; + } + + can enter_B with B entry { + report here del --> [-->]; + } +} + +walker update_graph { + can enter with `root entry { + report here; + visit [-->]; + } + + can enter_A with A entry { + here.val = 1; + report here; + visit [-->]; + } + + can enter_B with B entry { + here.val = 2; + report here; + visit [-->]; + } +} + +walker create_nested_node { + can enter_root with `root entry { + n = Nested( + val=0, + arr=[], + json={}, + parent=Parent( + val=1, + arr=[1], + json={"a": 1}, + child=Child( + val=2, + arr=[1, 2], + json={"a": 1, "b": 2}, + enum_field = Enum.C + ), + enum_field = Enum.B + ), + enum_field = Enum.A + ); + here ++> n; + report n; + } +} + +walker update_nested_node { + can enter_root with `root entry { + nested = [-->(`?Nested)][0]; + nested.parent.child.json["c"] = 3; + nested.parent.child.arr.append(3); + nested.parent.child.val = 3; + nested.parent.child.enum_field = Enum.A; + nested.parent.json["b"] = 2; + nested.parent.arr.append(2); + nested.parent.val = 2; + nested.parent.enum_field = Enum.C; + nested.json["a"] = 1; + nested.arr.append(1); + nested.val = 1; + nested.enum_field = Enum.B; + report nested; + } + + can enter_nested with Nested entry { + here.parent.child.json["c"] = 3; + here.parent.child.arr.append(3); + here.parent.child.val = 3; + here.parent.child.enum_field = Enum.A; + here.parent.json["b"] = 2; + here.parent.arr.append(2); + here.parent.val = 2; + here.parent.enum_field = Enum.C; + here.json["a"] = 1; + here.arr.append(1); + here.val = 1; + here.enum_field = Enum.B; + report here; + } +} + +walker detach_nested_node { + can enter_root with `root entry { + report here del--> [-->(`?Nested)]; + } +} + +walker visit_nested_node { + can enter_root with `root entry { + nesteds = [-->(`?Nested)]; + if nesteds { + report [-->(`?Nested)][0]; + } else { + report nesteds; + } + } + + can enter_nested with Nested entry { + report here; + } +} + +walker delete_nested_node { + can enter_root with `root entry { + nested = [-->(`?Nested)][0]; + nested.__jac__.destroy(); + # nested.__jac__.apply(); + + report [-->(`?Nested)]; + } +} + +walker delete_nested_edge { + can enter_root with `root entry { + nested_edge = :e:[-->][0]; + nested_edge.__jac__.destroy(); + + report [-->(`?Nested)]; + } +} + +walker allow_other_root_access { + has root_id: str, level: int | str = 0, via_all: bool = False; + + can enter_root with `root entry { + if self.via_all { + here.__jac__.unrestrict(self.level); + } else { + here.__jac__.allow_root(UUID(self.root_id), self.level); + } + } + + can enter_nested with Nested entry { + if self.via_all { + here.__jac__.unrestrict(self.level); + } else { + here.__jac__.allow_root(UUID(self.root_id), self.level); + } + } +} + +walker disallow_other_root_access { + has root_id: str, via_all: bool = False; + + can enter_root with `root entry { + if self.via_all { + here.__jac__.restrict(); + } else { + here.__jac__.disallow_root(UUID(self.root_id)); + } + } + + can enter_nested with Nested entry { + if self.via_all { + here.__jac__.restrict(); + } else { + here.__jac__.disallow_root(UUID(self.root_id)); + } + } +} + +################################################################# +# ENDPOINT CUSTOMIZATIONS # +################################################################# + +walker post_no_body {} + +walker post_with_body { + has a: str; +} + +walker get_no_body { + class __specs__ { + has methods: list = ["get"]; + } +} + +walker get_with_query { + has a: str; + + class __specs__ { + has methods: list = ["get"], as_query: list = ["a"]; + } +} + +walker get_all_query { + has a: str; + has b: str; + + class __specs__ { + has methods: list = ["get"], as_query: list = "*", auth: bool = False; + } +} + +walker post_path_var { + has a: str; + + class __specs__ { + has path: str = "/{a}", methods: list = ["post", "get"]; + } +} + +walker combination1 { + has a: str; + has b: str; + has c: str; + + class __specs__ { + has methods: list = ["post", "get"], as_query: list = ["a", "b"]; + } +} + + +walker combination2 { + has a: str; + has b: str; + has c: str; + + class __specs__ { + has path: str = "/{a}", methods: list = ["post", "get", "put", "patch", "delete", "head", "trace", "options"], as_query: list = ["b"]; + } +} + +walker post_with_file { + has single: UploadFile; + has multiple: list[UploadFile]; + has singleOptional: UploadFile | None = None; + + + can enter with `root entry { + print(self.single); + print(self.multiple); + print(self.singleOptional); + } + + class __specs__ {} +} + +walker post_with_body_and_file { + has val: int; + has single: UploadFile; + has multiple: list[UploadFile]; + has optional_val: int = 0; + + can enter with `root entry { + print(self.val); + print(self.optional_val); + print(self.single); + print(self.multiple); + } + + class __specs__ { + has auth: bool = False; + } +} + + +walker custom_status_code { + has status: int; + + can enter with `root entry { + Jac.get_context().status = self.status; + } +} \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py new file mode 100644 index 0000000000..6c93a68fbc --- /dev/null +++ b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py @@ -0,0 +1,416 @@ +"""JacLang Jaseci Unit Test.""" + +from contextlib import suppress +from json import load +from os import getenv, path +from shelve import open as shelf +from typing import Literal, overload +from unittest.async_case import IsolatedAsyncioTestCase + +from httpx import get, post + +from jaclang import jac_import +from jaclang.runtimelib.context import ExecutionContext + + +class SimpleGraphTest(IsolatedAsyncioTestCase): + """JacLang Jaseci Feature Tests.""" + + def __init__(self, methodName: str = "runTest") -> None: # noqa: N803 + """Overide Init.""" + super().__init__(methodName) + + (base, ignored) = path.split(__file__) + base = base if base else "./" + + jac_import( + target="simple_graph_mini", + base_path=base, + cachable=True, + override_name="__main__", + ) + + async def asyncSetUp(self) -> None: + """Reset DB and wait for server.""" + self.host = "http://0.0.0.0:8001" + self.database = getenv("DATABASE", "database") + count = 0 + while True: + if count > 5: + self.check_server() + break + else: + with suppress(Exception): + self.check_server() + break + count += 1 + + async def asyncTearDown(self) -> None: + """Clean up DB.""" + self.clear_db() + + def clear_db(self) -> None: + """Clean DB.""" + with shelf(self.database) as sh: + sh.clear() + sh.sync() + + @overload + def post_api(self, api: str, json: dict | None = None) -> dict: + pass + + @overload + def post_api( + self, + api: str, + json: dict | None = None, + expect_error: Literal[True] = True, + ) -> int: + pass + + def post_api( + self, + api: str, + json: dict | None = None, + expect_error: bool = False, + ) -> dict | int: + """Call walker post API.""" + res = post(f"{self.host}/{api}", json=json) + + if not expect_error: + res.raise_for_status() + return res.json() + else: + return res.status_code + + def check_server(self) -> None: + """Retrieve OpenAPI Specs JSON.""" + res = get(f"{self.host}/") + res.raise_for_status() + self.assertEqual(200, res.status_code) + + def trigger_openapi_specs_test(self) -> None: + """Test OpenAPI Specs.""" + res = get(f"{self.host}/openapi.json", timeout=1) + res.raise_for_status() + + with open("jac_cloud/tests/openapi_specs_mini.json") as file: + self.assertEqual(load(file), res.json()) + + def trigger_create_graph_test(self) -> None: + """Test Graph Creation.""" + res = self.post_api("create_graph") + + self.assertEqual(200, res["status"]) + self.assertEqual(4, len(res["reports"])) + + root_node = res["reports"].pop(0) + self.assertEqual("00000000000000000000000000000000", root_node["id"]) + self.assertEqual({}, root_node["context"]) + + for idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx}, report["context"]) + + def trigger_traverse_graph_test(self) -> None: + """Test Graph Traversion.""" + res = self.post_api("traverse_graph") + + self.assertEqual(200, res["status"]) + self.assertEqual(4, len(res["reports"])) + + root_node = res["reports"].pop(0) + self.assertEqual("00000000000000000000000000000000", root_node["id"]) + self.assertEqual({}, root_node["context"]) + + for idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx}, report["context"]) + + res = self.post_api(f"traverse_graph/{report["id"]}") + self.assertEqual(200, res["status"]) + for _idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx + _idx}, report["context"]) + + def trigger_detach_node_test(self) -> None: + """Test detach node.""" + res = self.post_api("detach_node") + + self.assertEqual(200, res["status"]) + self.assertEqual([True], res["reports"]) + + res = self.post_api("traverse_graph") + self.assertEqual(200, res["status"]) + self.assertEqual(3, len(res["reports"])) + + root_node = res["reports"].pop(0) + self.assertEqual("00000000000000000000000000000000", root_node["id"]) + self.assertEqual({}, root_node["context"]) + + for idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx}, report["context"]) + + res = self.post_api(f"traverse_graph/{report["id"]}") + self.assertEqual(200, res["status"]) + for _idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx + _idx}, report["context"]) + + def trigger_update_graph_test(self) -> None: + """Test update graph.""" + res = self.post_api("update_graph") + + self.assertEqual(200, res["status"]) + self.assertEqual(3, len(res["reports"])) + + root_node = res["reports"].pop(0) + self.assertEqual("00000000000000000000000000000000", root_node["id"]) + self.assertEqual({}, root_node["context"]) + + for idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx + 1}, report["context"]) + + res = self.post_api("traverse_graph") + + self.assertEqual(200, res["status"]) + self.assertEqual(3, len(res["reports"])) + + root_node = res["reports"].pop(0) + self.assertEqual("00000000000000000000000000000000", root_node["id"]) + self.assertEqual({}, root_node["context"]) + + for idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx + 1}, report["context"]) + + res = self.post_api(f"traverse_graph/{report["id"]}") + self.assertEqual(200, res["status"]) + for _idx, report in enumerate(res["reports"]): + self.assertEqual({"val": idx + _idx + 1}, report["context"]) + + def trigger_create_nested_node_test(self, manual: bool = False) -> None: + """Test create nested node.""" + res = self.post_api("create_nested_node") + + self.assertEqual(200, res["status"]) + self.assertEqual( + { + "val": 0, + "arr": [], + "json": {}, + "parent": { + "val": 1, + "arr": [1], + "json": {"a": 1}, + "child": { + "val": 2, + "arr": [1, 2], + "json": {"a": 1, "b": 2}, + "enum_field": "c", + }, + "enum_field": "b", + }, + "enum_field": "a", + }, + res["reports"][0]["context"], + ) + + def trigger_update_nested_node_test(self) -> None: + """Test update nested node.""" + for walker in ["update_nested_node", "visit_nested_node"]: + res = self.post_api(walker) + self.assertEqual(200, res["status"]) + self.assertEqual( + { + "val": 1, + "arr": [1], + "json": {"a": 1}, + "parent": { + "val": 2, + "arr": [1, 2], + "json": {"a": 1, "b": 2}, + "child": { + "val": 3, + "arr": [1, 2, 3], + "json": {"a": 1, "b": 2, "c": 3}, + "enum_field": "a", + }, + "enum_field": "c", + }, + "enum_field": "b", + }, + res["reports"][0]["context"], + ) + + def trigger_detach_nested_node_test(self, manual: bool = False) -> None: + """Test detach nested node.""" + res = self.post_api("detach_nested_node") + self.assertEqual(200, res["status"]) + self.assertEqual([True], res["reports"]) + + res = self.post_api("visit_nested_node") + self.assertEqual(200, res["status"]) + self.assertEqual([[]], res["reports"]) + + def trigger_delete_nested_node_test(self, manual: bool = False) -> None: + """Test create nested node.""" + res = self.post_api("delete_nested_node") + self.assertEqual(200, res["status"]) + self.assertEqual([[]], res["reports"]) + + res = self.post_api("visit_nested_node") + self.assertEqual(200, res["status"]) + self.assertEqual([[]], res["reports"]) + + def trigger_delete_nested_edge_test(self, manual: bool = False) -> None: + """Test create nested node.""" + res = self.post_api("delete_nested_edge") + self.assertEqual(200, res["status"]) + self.assertEqual([[]], res["reports"]) + + res = self.post_api("visit_nested_node") + self.assertEqual(200, res["status"]) + self.assertEqual([[]], res["reports"]) + + async def nested_count_should_be(self, node: int, edge: int) -> None: + """Test nested node count.""" + jctx = ExecutionContext.create(session=self.database) + + node_count = 0 + edge_count = 0 + + for val in jctx.mem.__shelf__.values(): + if val.architype.__class__.__name__ == "Nested": + node_count += 1 + elif ( + (source := getattr(val, "source", None)) + and (target := getattr(val, "target", None)) + and ( + source.architype.__class__.__name__ == "Nested" + or target.architype.__class__.__name__ == "Nested" + ) + ): + edge_count += 1 + + self.assertEqual(node, node_count) + self.assertEqual(edge, edge_count) + + jctx.close() + + async def trigger_custom_status_code(self) -> None: + """Test custom status code.""" + for acceptable_code in [200, 201, 202, 203, 205, 206, 207, 208, 226]: + res = self.post_api("custom_status_code", {"status": acceptable_code}) + self.assertEqual(acceptable_code, res["status"]) + self.assertEqual([], res["reports"]) + + for error_code in [ + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 418, + 421, + 422, + 423, + 424, + 425, + 426, + 428, + 429, + 431, + 451, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 510, + 511, + ]: + self.assertEqual( + error_code, + self.post_api( + "custom_status_code", {"status": error_code}, expect_error=True + ), + ) + + for invalid_code in [ + 100, + 101, + 102, + 103, + 204, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + ]: + self.assertRaises( + Exception, self.post_api, "custom_status_code", {"status": invalid_code} + ) + + async def test_all_features(self) -> None: + """Test Full Features.""" + self.trigger_openapi_specs_test() + + self.trigger_create_graph_test() + self.trigger_traverse_graph_test() + self.trigger_detach_node_test() + self.trigger_update_graph_test() + + ################################################### + # VIA DETACH # + ################################################### + + self.clear_db() + + await self.nested_count_should_be(node=0, edge=0) + + self.trigger_create_nested_node_test() + await self.nested_count_should_be(node=1, edge=1) + + self.trigger_update_nested_node_test() + self.trigger_detach_nested_node_test() + await self.nested_count_should_be(node=0, edge=0) + + ################################################### + # VIA DESTROY # + ################################################### + + self.trigger_create_nested_node_test() + await self.nested_count_should_be(node=1, edge=1) + + self.trigger_delete_nested_node_test() + await self.nested_count_should_be(node=0, edge=0) + + self.trigger_create_nested_node_test() + await self.nested_count_should_be(node=1, edge=1) + + self.trigger_delete_nested_edge_test() + await self.nested_count_should_be(node=0, edge=0) + + ################################################### + # CUSTOM STATUS # + ################################################### + + await self.trigger_custom_status_code()