From 84e7dd092497e4476463bbb12d435d4db9682030 Mon Sep 17 00:00:00 2001 From: "Alexie (Boyong) Madolid" Date: Fri, 20 Sep 2024 00:48:32 +0800 Subject: [PATCH] [JAC-MINI]: Initial --- jac-mini/.flake8 | 5 + jac-mini/.gitignore | 53 + jac-mini/.pre-commit-config.yaml | 27 + jac-mini/LICENSE | 21 + jac-mini/README.md | 11 + jac-mini/jac_mini/__init__.py | 1 + jac-mini/jac_mini/plugin/__init__.py | 1 + jac-mini/jac_mini/plugin/cli.py | 198 ++ jac-mini/jac_mini/plugin/jaseci.py | 16 + jac-mini/jac_mini/tests/__init__.py | 1 + jac-mini/jac_mini/tests/openapi_specs.json | 1972 ++++++++++++++++++ jac-mini/jac_mini/tests/simple_graph.jac | 338 +++ jac-mini/jac_mini/tests/test_simple_graph.py | 334 +++ jac-mini/mypy.ini | 2 + jac-mini/setup.py | 32 + jac/jaclang/runtimelib/memory.py | 2 +- 16 files changed, 3013 insertions(+), 1 deletion(-) create mode 100644 jac-mini/.flake8 create mode 100644 jac-mini/.gitignore create mode 100644 jac-mini/.pre-commit-config.yaml create mode 100644 jac-mini/LICENSE create mode 100644 jac-mini/README.md create mode 100644 jac-mini/jac_mini/__init__.py create mode 100644 jac-mini/jac_mini/plugin/__init__.py create mode 100644 jac-mini/jac_mini/plugin/cli.py create mode 100644 jac-mini/jac_mini/plugin/jaseci.py create mode 100644 jac-mini/jac_mini/tests/__init__.py create mode 100644 jac-mini/jac_mini/tests/openapi_specs.json create mode 100644 jac-mini/jac_mini/tests/simple_graph.jac create mode 100644 jac-mini/jac_mini/tests/test_simple_graph.py create mode 100644 jac-mini/mypy.ini create mode 100644 jac-mini/setup.py diff --git a/jac-mini/.flake8 b/jac-mini/.flake8 new file mode 100644 index 0000000000..6bb3fd7804 --- /dev/null +++ b/jac-mini/.flake8 @@ -0,0 +1,5 @@ +[flake8] +exclude = fixtures, __jac_gen__ +plugins = flake8_import_order, flake8_docstrings, flake8_comprehensions, flake8_bugbear, flake8_annotations, pep8_naming, flake8_simplify +max-line-length = 120 +ignore = E203, W503, ANN101, ANN102 \ No newline at end of file diff --git a/jac-mini/.gitignore b/jac-mini/.gitignore new file mode 100644 index 0000000000..269f954165 --- /dev/null +++ b/jac-mini/.gitignore @@ -0,0 +1,53 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media +migrations +venv +__jac_gen__/ +*.jir + +# Distribution / packaging +.Python +play/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Others # +.env +.coverage +.session +.DS_Store +build +.vscode +*Zone.Identifier +.DS_Store +parser.out +codegen_output* + +*.rdb +node_modules +out.dot +out.txt + +# Mypy files # +.mypy_cache* +.jac_mypy_cache diff --git a/jac-mini/.pre-commit-config.yaml b/jac-mini/.pre-commit-config.yaml new file mode 100644 index 0000000000..722716f557 --- /dev/null +++ b/jac-mini/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-yaml + args: [--allow-multiple-documents] + - id: check-json + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + - repo: https://github.com/PyCQA/flake8 + rev: 7.1.0 + hooks: + - id: flake8 + args: ["--config=jac-mini/.flake8"] + additional_dependencies: [pep8-naming, flake8_import_order, flake8_docstrings, flake8_comprehensions, flake8_bugbear, flake8_annotations, flake8_simplify] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: [types-redis, motor-types] + exclude: 'venv|__jac_gen__|tests|setup.py' + args: + - --follow-imports=silent + - --ignore-missing-imports diff --git a/jac-mini/LICENSE b/jac-mini/LICENSE new file mode 100644 index 0000000000..6a050db5a4 --- /dev/null +++ b/jac-mini/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jaseci-Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/jac-mini/README.md b/jac-mini/README.md new file mode 100644 index 0000000000..4c5de336bb --- /dev/null +++ b/jac-mini/README.md @@ -0,0 +1,11 @@ +# **jac-cloud** + +### Installation +```bash +pip install -e . +``` +### Dependencies for Devs +```bash +pip install jaclang black pre-commit flake8 flake8_import_order flake8_docstrings flake8_comprehensions flake8_bugbear flake8_annotations pep8_naming flake8_simplify mypy pytest +pre-commit install +``` \ No newline at end of file diff --git a/jac-mini/jac_mini/__init__.py b/jac-mini/jac_mini/__init__.py new file mode 100644 index 0000000000..7e17226fb7 --- /dev/null +++ b/jac-mini/jac_mini/__init__.py @@ -0,0 +1 @@ +"""JacLang Jaseci.""" diff --git a/jac-mini/jac_mini/plugin/__init__.py b/jac-mini/jac_mini/plugin/__init__.py new file mode 100644 index 0000000000..2e15a9c023 --- /dev/null +++ b/jac-mini/jac_mini/plugin/__init__.py @@ -0,0 +1 @@ +"""Jaseci Plugins.""" diff --git a/jac-mini/jac_mini/plugin/cli.py b/jac-mini/jac_mini/plugin/cli.py new file mode 100644 index 0000000000..8ed0c6fe7b --- /dev/null +++ b/jac-mini/jac_mini/plugin/cli.py @@ -0,0 +1,198 @@ +"""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.cli.cmdreg import cmd_registry +from jaclang.plugin.default import hookimpl +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) -> dict[str, Any]: + """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 resp + + +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 ORJSONResponse(response(jctx.reports)) + + 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) + + +class JacCmd: + """Jac CLI.""" + + @staticmethod + @hookimpl + def create_cmd() -> None: + """Create Jac CLI cmds.""" + + @cmd_registry.register + def serve(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-mini/jac_mini/plugin/jaseci.py b/jac-mini/jac_mini/plugin/jaseci.py new file mode 100644 index 0000000000..f3a03a3898 --- /dev/null +++ b/jac-mini/jac_mini/plugin/jaseci.py @@ -0,0 +1,16 @@ +"""Jac Language Features.""" + +from typing import Any + +from jaclang.plugin.default import hookimpl +from jaclang.plugin.feature import JacFeature as Jac + + +class JacPlugin: + """Jaseci Implementations.""" + + @staticmethod + @hookimpl + def report(expr: Any) -> None: # noqa:ANN401 + """Jac's report stmt feature.""" + Jac.get_context().reports.append(expr) diff --git a/jac-mini/jac_mini/tests/__init__.py b/jac-mini/jac_mini/tests/__init__.py new file mode 100644 index 0000000000..ff15913d4a --- /dev/null +++ b/jac-mini/jac_mini/tests/__init__.py @@ -0,0 +1 @@ +"""Jaseci Unit Tests.""" diff --git a/jac-mini/jac_mini/tests/openapi_specs.json b/jac-mini/jac_mini/tests/openapi_specs.json new file mode 100644 index 0000000000..1d25ddb7fe --- /dev/null +++ b/jac-mini/jac_mini/tests/openapi_specs.json @@ -0,0 +1,1972 @@ +{ + "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" + } + } + } + } + } + } + } + }, + "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" + }, + "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-mini/jac_mini/tests/simple_graph.jac b/jac-mini/jac_mini/tests/simple_graph.jac new file mode 100644 index 0000000000..dfe0c40ba0 --- /dev/null +++ b/jac-mini/jac_mini/tests/simple_graph.jac @@ -0,0 +1,338 @@ +"""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; + } +} \ No newline at end of file diff --git a/jac-mini/jac_mini/tests/test_simple_graph.py b/jac-mini/jac_mini/tests/test_simple_graph.py new file mode 100644 index 0000000000..c18cf5fbe8 --- /dev/null +++ b/jac-mini/jac_mini/tests/test_simple_graph.py @@ -0,0 +1,334 @@ +"""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", + 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:8000" + 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_mini/tests/openapi_specs.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 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) diff --git a/jac-mini/mypy.ini b/jac-mini/mypy.ini new file mode 100644 index 0000000000..8c70998a96 --- /dev/null +++ b/jac-mini/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +exclude = venv|__jac_gen__|tests|setup.py|jaclang diff --git a/jac-mini/setup.py b/jac-mini/setup.py new file mode 100644 index 0000000000..589bb62291 --- /dev/null +++ b/jac-mini/setup.py @@ -0,0 +1,32 @@ +"""Jaclang setup file.""" + +from setuptools import find_packages, setup + +VERSION = "0.0.1" + +setup( + name="jac-mini", + version=VERSION, + packages=find_packages(include=["jac_mini", "jac_mini.*"]), + install_requires=[ + "jaclang~=0.7.17", + "fastapi~=0.111.0", + "pydantic~=2.8.2", + "python-dotenv~=1.0.1", + "uvicorn~=0.30.1", + "orjson~=3.10.6", + "python-multipart~=0.0.9", + "asyncer~=0.0.8", + "fakeredis~=2.24.1", + ], + package_data={}, + entry_points={ + "jac": [ + "jac = jac_mini.plugin.jaseci:JacPlugin", + "serve = jac_mini.plugin.cli:JacCmd", + ], + }, + author="Jason Mars", + author_email="jason@jaseci.org", + url="https://github.com/Jaseci-Labs/jac-mini", +) diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index 2874053181..a65576468c 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -106,7 +106,7 @@ def close(self) -> None: if root.has_write_access(d): if hash(dumps(p_d.access)) != hash(dumps(d.access)): p_d.access = d.access - if hash(dumps(d.architype)) != hash(dumps(d.architype)): + if hash(dumps(p_d.architype)) != hash(dumps(d.architype)): p_d.architype = d.architype self.__shelf__[_id] = p_d