diff --git a/.github/workflows/test-cloud.yml b/.github/workflows/test-cloud.yml index bfb3510081..66159dc5f8 100644 --- a/.github/workflows/test-cloud.yml +++ b/.github/workflows/test-cloud.yml @@ -18,7 +18,6 @@ jobs: working-directory: jac-cloud env: SHOW_ENDPOINT_RETURNS: true - REDIS_HOST: redis://localhost services: redis: image: redis @@ -52,13 +51,11 @@ jobs: pip install -e . pip install pytest - - name: Run tests - run: | - jac clean && jac tool gen_parser - jac serve jac_cloud/tests/simple_graph_mini.jac --port 8001 & - sleep 3 && pytest -s jac_cloud/tests/test_simple_graph_mini.py - - export DATABASE_HOST="mongodb://localhost/?retryWrites=true&w=majority" - jac clean && jac tool gen_parser - jac serve jac_cloud/tests/simple_graph.jac --port 8000 & - sleep 3 && pytest -s jac_cloud/tests/test_simple_graph.py + - name: Run tests without mongodb and redis + run: pytest -sx + - name: Run tests with mongodb and redis + run: pytest -sx + env: + SHOW_ENDPOINT_RETURNS: true + DATABASE_HOST: mongodb://localhost/?retryWrites=true&w=majority + REDIS_HOST: redis://localhost diff --git a/jac-cloud/jac_cloud/jaseci/datasources/__init__.py b/jac-cloud/jac_cloud/jaseci/datasources/__init__.py index 424f107e23..b478df4ec9 100644 --- a/jac-cloud/jac_cloud/jaseci/datasources/__init__.py +++ b/jac-cloud/jac_cloud/jaseci/datasources/__init__.py @@ -1,11 +1,13 @@ """Jaseci Datasources.""" from .collection import Collection +from .localdb import MontyClient from .redis import CodeRedis, Redis, TokenRedis __all__ = [ "Collection", + "MontyClient", "CodeRedis", "Redis", "TokenRedis", diff --git a/jac-cloud/jac_cloud/jaseci/datasources/collection.py b/jac-cloud/jac_cloud/jaseci/datasources/collection.py index 5425268e39..e4b971b5d7 100644 --- a/jac-cloud/jac_cloud/jaseci/datasources/collection.py +++ b/jac-cloud/jac_cloud/jaseci/datasources/collection.py @@ -46,6 +46,9 @@ ) from pymongo.server_api import ServerApi +from .localdb import MontyClient, set_storage +from ..utils import logger + T = TypeVar("T") @@ -137,16 +140,23 @@ def __documents__(cls, docs: Cursor) -> Generator[T, None, None]: @staticmethod def get_client() -> MongoClient: """Return pymongo.database.Database for mongodb connection.""" - if not isinstance(Collection.__client__, MongoClient): - Collection.__client__ = MongoClient( - getenv( - "DATABASE_HOST", - "mongodb://localhost/?retryWrites=true&w=majority", - ), - server_api=ServerApi("1"), - ) - - return Collection.__client__ + if (client := Collection.__client__) is None: + if host := getenv("DATABASE_HOST"): + client = Collection.__client__ = MongoClient( + host, + server_api=ServerApi("1"), + ) + else: + logger.info("DATABASE_HOST is not available! Using LocalDB...") + set_storage( + repository="mydatabase", + storage="sqlite", + mongo_version="4.4", + use_bson=True, + ) + client = Collection.__client__ = MontyClient("mydatabase") + + return client @staticmethod def get_session() -> ClientSession: diff --git a/jac-cloud/jac_cloud/jaseci/datasources/localdb.py b/jac-cloud/jac_cloud/jaseci/datasources/localdb.py new file mode 100644 index 0000000000..d60c9f88c3 --- /dev/null +++ b/jac-cloud/jac_cloud/jaseci/datasources/localdb.py @@ -0,0 +1,163 @@ +"""Monty Implementations.""" + +from typing import Any + +from montydb import MontyClient as _MontyClient, set_storage # type: ignore[import-untyped] +from montydb.collection import MontyCollection as _MontyCollection # type: ignore[import-untyped] +from montydb.database import MontyDatabase as _MontyDatabase # type: ignore[import-untyped] + +from pymongo import DeleteMany, DeleteOne, InsertOne, UpdateMany, UpdateOne +from pymongo.cursor import Cursor +from pymongo.results import BulkWriteResult, InsertManyResult, InsertOneResult + + +class MontyClientSession: + """Monty Client Session.""" + + def start_transaction(self) -> "MontyClientSession": + """Start transaction.""" + return self + + def abort_transaction(self) -> None: + """Abort transaction.""" + + def commit_transaction(self) -> None: + """Commit transaction.""" + + def __enter__(self) -> "MontyClientSession": + """Enter execution.""" + return self + + def __exit__( + self, exc_type: Any, exc_val: Any, exc_tb: Any # noqa: ANN401 + ) -> None: + """Exit execution.""" + pass + + +class MontyCollection(_MontyCollection): + """Monty Collection.""" + + def __init__(self, col: _MontyCollection) -> None: + """Override Init.""" + self.__dict__.update(col.__dict__) + + def create_indexes(self, *args, **kwargs: Any) -> None: # noqa: ANN401, ANN002 + """Bypass Create Indexes.""" + pass + + def insert_one( + self, document: dict, session: MontyClientSession | None = None + ) -> InsertOneResult: + """Override insert_one.""" + return super().insert_one(document) + + def insert_many( + self, + documents: list[dict], + session: MontyClientSession | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> InsertManyResult: + """Override insert_many.""" + return super().insert_many(documents, **kwargs) + + def find( + self, + filter: dict | None = None, + projection: dict | None = None, + session: MontyClientSession | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> Cursor: + """Override find.""" + return super().find(filter, **kwargs) + + def find_one( + self, + filter: dict | None = None, + projection: dict | None = None, + session: MontyClientSession | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> Any | None: + """Override find_one.""" + return super().find_one(filter, **kwargs) + + def count_documents( + self, + filter: dict, + session: MontyClientSession | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> int: + """Override count_documents.""" + return super().count_documents(filter, **kwargs) + + def bulk_write( + self, + ops: list[InsertOne | DeleteMany | DeleteOne | UpdateMany | UpdateOne], + ordered: bool = True, + session: MontyClientSession | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> BulkWriteResult: + """Bulk write operations.""" + deleted_count = 0 + inserted_count = 0 + modified_count = 0 + + for op in ops: + match op: + case InsertOne(): + if self.insert_one(op._doc).inserted_id: + inserted_count += 1 + case DeleteMany(): + deleted_count += self.delete_many(op._filter).deleted_count + case DeleteOne(): + deleted_count += self.delete_one(op._filter).deleted_count + case UpdateMany(): + modified_count += self.update_many( + op._filter, op._doc + ).modified_count + case UpdateOne(): + modified_count += self.update_one( + op._filter, op._doc + ).modified_count + case _: + pass + + return BulkWriteResult( + { + "bulk_api_result": {}, + "deleted_count": deleted_count, + "inserted_count": inserted_count, + "matched_count": deleted_count + modified_count, + "modified_count": modified_count, + "upserted_count": 0, + "upserted_ids": {}, + }, + True, + ) + + +class MontyDatabase(_MontyDatabase): + """Monty Database.""" + + def __init__(self, db: _MontyDatabase) -> None: + """Override Init.""" + self.__dict__.update(db.__dict__) + + def get_collection(self, name: str) -> MontyCollection: + """Get Collection.""" + return MontyCollection(super().get_collection(name)) # noqa: B009 + + +class MontyClient(_MontyClient): + """Monty Client.""" + + def get_database(self, name: str) -> MontyDatabase: + """Get local database.""" + return MontyDatabase(super().get_database(name)) + + def start_session(self) -> MontyClientSession: + """Start session.""" + return MontyClientSession() + + +__all__ = ["MontyClient", "set_storage"] diff --git a/jac-cloud/jac_cloud/jaseci/datasources/redis.py b/jac-cloud/jac_cloud/jaseci/datasources/redis.py index 52be816451..e8022cab27 100644 --- a/jac-cloud/jac_cloud/jaseci/datasources/redis.py +++ b/jac-cloud/jac_cloud/jaseci/datasources/redis.py @@ -46,6 +46,7 @@ def get_rd() -> _Redis: password=getenv("REDIS_PASS"), ) else: + logger.info("REDIS_HOST is not available! Using FakeRedis...") Redis.__redis__ = FakeRedis() return Redis.__redis__ diff --git a/jac-cloud/jac_cloud/plugin/cli.py b/jac-cloud/jac_cloud/plugin/cli.py index e8dba1a9ac..b78efccc77 100644 --- a/jac-cloud/jac_cloud/plugin/cli.py +++ b/jac-cloud/jac_cloud/plugin/cli.py @@ -13,7 +13,6 @@ from pymongo.errors import ConnectionFailure, OperationFailure -from .mini.cli_mini import serve_mini from ..core.architype import BulkWrite, NodeAnchor from ..core.context import SUPER_ROOT_ID from ..jaseci.datasources import Collection @@ -31,10 +30,6 @@ 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.""" diff --git a/jac-cloud/jac_cloud/plugin/mini/__init__.py b/jac-cloud/jac_cloud/plugin/mini/__init__.py deleted file mode 100644 index 1fa8f770d8..0000000000 --- a/jac-cloud/jac_cloud/plugin/mini/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""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 deleted file mode 100644 index 3d4db6718f..0000000000 --- a/jac-cloud/jac_cloud/plugin/mini/cli_mini.py +++ /dev/null @@ -1,201 +0,0 @@ -"""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 APIRouter, Depends, FastAPI, File, Response, UploadFile, status -from fastapi.responses import ORJSONResponse - -from jac_cloud.jaseci.utils import populate_yaml_specs - -from jaclang import jac_import -from jaclang.plugin.feature import JacFeature as Jac -from jaclang.runtimelib.architype import ( - Anchor, - Architype, - 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(router: APIRouter, cls: Type[WalkerArchitype]) -> None: - """Generate FastAPI endpoint based on WalkerArchitype class.""" - body: dict[str, Any] = {} - files: dict[str, Any] = {} - - hintings = get_type_hints(cls) - if is_dataclass(cls): - for f in fields(cls): - f_name = f.name - f_type = hintings[f_name] - if f_type in FILE_TYPES: - files[f_name] = gen_model_field(f_type, f, True) - else: - consts = gen_model_field(f_type, f) - body[f_name] = consts - - payload: dict[str, Any] = { - "files": ( - 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) - - Jac.spawn_call(cls(**body, **pl["files"]), jctx.entry_node.architype) - jctx.close() - - if jctx.custom is not MISSING: - return jctx.custom - - return response(jctx.reports, getattr(jctx, "status", 200)) - - def api_root( - payload: payload_model = Depends(), # type: ignore # noqa: B008 - ) -> Response: - return api_entry(None, payload) - - router.post(url := f"/{cls.__name__}", summary=url)(api_root) - router.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, sem_ir=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() - - populate_yaml_specs(app) - - healtz_router = APIRouter(prefix="/healthz", tags=["monitoring"]) - walker_router = APIRouter(prefix="/walker", tags=["walker"]) - - @healtz_router.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(walker_router, obj) - - app.include_router(healtz_router) - app.include_router(walker_router) - - run(app, host=host, port=port) - - JacMachine.detach() diff --git a/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml b/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml deleted file mode 100644 index 32a3b998f8..0000000000 --- a/jac-cloud/jac_cloud/tests/openapi_specs_mini.yaml +++ /dev/null @@ -1,1726 +0,0 @@ -components: - schemas: - Body_api_entry_walker_post_with_body_and_file__node__post: - properties: - body: - format: binary - title: Body - type: string - multiple: - items: - format: binary - type: string - title: Multiple - type: array - single: - format: binary - title: Single - type: string - required: - - body - - single - - multiple - title: Body_api_entry_walker_post_with_body_and_file__node__post - type: object - Body_api_entry_walker_post_with_file__node__post: - properties: - multiple: - items: - format: binary - type: string - title: Multiple - type: array - single: - format: binary - title: Single - type: string - singleOptional: - anyOf: - - format: binary - type: string - - type: 'null' - title: Singleoptional - required: - - single - - multiple - title: Body_api_entry_walker_post_with_file__node__post - type: object - Body_api_root_walker_post_with_body_and_file_post: - properties: - body: - format: binary - title: Body - type: string - multiple: - items: - format: binary - type: string - title: Multiple - type: array - single: - format: binary - title: Single - type: string - required: - - body - - single - - multiple - title: Body_api_root_walker_post_with_body_and_file_post - type: object - Body_api_root_walker_post_with_file_post: - properties: - multiple: - items: - format: binary - type: string - title: Multiple - type: array - single: - format: binary - title: Single - type: string - singleOptional: - anyOf: - - format: binary - type: string - - type: 'null' - title: Singleoptional - required: - - single - - multiple - title: Body_api_root_walker_post_with_file_post - type: object - HTTPValidationError: - properties: - detail: - items: - $ref: '#/components/schemas/ValidationError' - title: Detail - type: array - title: HTTPValidationError - type: object - ValidationError: - properties: - loc: - items: - anyOf: - - type: string - - type: integer - title: Location - type: array - msg: - title: Message - type: string - type: - title: Error Type - type: string - required: - - loc - - msg - - type - title: ValidationError - type: object - allow_other_root_access_body_model: - properties: - level: - anyOf: - - type: integer - - type: string - title: Level - root_id: - title: Root Id - type: string - via_all: - title: Via All - type: boolean - required: - - root_id - title: allow_other_root_access_body_model - type: object - combination1_body_model: - properties: - a: - title: A - type: string - b: - title: B - type: string - c: - title: C - type: string - d: - title: D - type: string - required: - - a - - b - - c - title: combination1_body_model - type: object - combination2_body_model: - properties: - a: - title: A - type: string - b: - title: B - type: string - c: - title: C - type: string - d: - title: D - type: string - e: - title: E - type: string - required: - - a - - b - - c - title: combination2_body_model - type: object - custom_status_code_body_model: - properties: - status: - title: Status - type: integer - required: - - status - title: custom_status_code_body_model - type: object - disallow_other_root_access_body_model: - properties: - root_id: - title: Root Id - type: string - via_all: - title: Via All - type: boolean - required: - - root_id - title: disallow_other_root_access_body_model - type: object - get_all_query_body_model: - properties: - a: - title: A - type: string - b: - title: B - type: string - required: - - a - - b - title: get_all_query_body_model - type: object - get_with_query_body_model: - properties: - a: - title: A - type: string - required: - - a - title: get_with_query_body_model - type: object - post_all_excluded_body_model: - properties: - a: - title: A - type: string - b: - title: B - type: string - title: post_all_excluded_body_model - type: object - post_partial_excluded_body_model: - properties: - a: - title: A - type: string - b: - title: B - type: string - required: - - a - title: post_partial_excluded_body_model - type: object - post_path_var_body_model: - properties: - a: - title: A - type: string - required: - - a - title: post_path_var_body_model - type: object - post_with_body_body_model: - properties: - a: - title: A - type: string - required: - - a - title: post_with_body_body_model - type: object -info: - title: FastAPI - version: 0.1.0 -openapi: 3.1.0 -paths: - /healthz: - get: - description: Healthz API. - operationId: healthz_healthz_get - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: Healthz - tags: - - monitoring - /walker/allow_other_root_access: - post: - operationId: api_root_walker_allow_other_root_access_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/allow_other_root_access_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /allow_other_root_access - tags: - - walker - /walker/allow_other_root_access/{node}: - post: - operationId: api_entry_walker_allow_other_root_access__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/allow_other_root_access_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /allow_other_root_access/{node} - tags: - - walker - /walker/check_populated_graph: - post: - operationId: api_root_walker_check_populated_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /check_populated_graph - tags: - - walker - /walker/check_populated_graph/{node}: - post: - operationId: api_entry_walker_check_populated_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /check_populated_graph/{node} - tags: - - walker - /walker/combination1: - post: - operationId: api_root_walker_combination1_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/combination1_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /combination1 - tags: - - walker - /walker/combination1/{node}: - post: - operationId: api_entry_walker_combination1__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/combination1_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /combination1/{node} - tags: - - walker - /walker/combination2: - post: - operationId: api_root_walker_combination2_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/combination2_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /combination2 - tags: - - walker - /walker/combination2/{node}: - post: - operationId: api_entry_walker_combination2__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/combination2_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /combination2/{node} - tags: - - walker - /walker/create_graph: - post: - operationId: api_root_walker_create_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /create_graph - tags: - - walker - /walker/create_graph/{node}: - post: - operationId: api_entry_walker_create_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /create_graph/{node} - tags: - - walker - /walker/create_nested_node: - post: - operationId: api_root_walker_create_nested_node_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /create_nested_node - tags: - - walker - /walker/create_nested_node/{node}: - post: - operationId: api_entry_walker_create_nested_node__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /create_nested_node/{node} - tags: - - walker - /walker/custom_report: - post: - operationId: api_root_walker_custom_report_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /custom_report - tags: - - walker - /walker/custom_report/{node}: - post: - operationId: api_entry_walker_custom_report__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /custom_report/{node} - tags: - - walker - /walker/custom_status_code: - post: - operationId: api_root_walker_custom_status_code_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/custom_status_code_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /custom_status_code - tags: - - walker - /walker/custom_status_code/{node}: - post: - operationId: api_entry_walker_custom_status_code__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/custom_status_code_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /custom_status_code/{node} - tags: - - walker - /walker/delete_nested_edge: - post: - operationId: api_root_walker_delete_nested_edge_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /delete_nested_edge - tags: - - walker - /walker/delete_nested_edge/{node}: - post: - operationId: api_entry_walker_delete_nested_edge__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /delete_nested_edge/{node} - tags: - - walker - /walker/delete_nested_node: - post: - operationId: api_root_walker_delete_nested_node_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /delete_nested_node - tags: - - walker - /walker/delete_nested_node/{node}: - post: - operationId: api_entry_walker_delete_nested_node__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /delete_nested_node/{node} - tags: - - walker - /walker/detach_nested_node: - post: - operationId: api_root_walker_detach_nested_node_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /detach_nested_node - tags: - - walker - /walker/detach_nested_node/{node}: - post: - operationId: api_entry_walker_detach_nested_node__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /detach_nested_node/{node} - tags: - - walker - /walker/detach_node: - post: - operationId: api_root_walker_detach_node_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /detach_node - tags: - - walker - /walker/detach_node/{node}: - post: - operationId: api_entry_walker_detach_node__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /detach_node/{node} - tags: - - walker - /walker/different_return: - post: - operationId: api_root_walker_different_return_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /different_return - tags: - - walker - /walker/different_return/{node}: - post: - operationId: api_entry_walker_different_return__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /different_return/{node} - tags: - - walker - /walker/disallow_other_root_access: - post: - operationId: api_root_walker_disallow_other_root_access_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/disallow_other_root_access_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /disallow_other_root_access - tags: - - walker - /walker/disallow_other_root_access/{node}: - post: - operationId: api_entry_walker_disallow_other_root_access__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/disallow_other_root_access_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /disallow_other_root_access/{node} - tags: - - walker - /walker/get_all_query: - post: - operationId: api_root_walker_get_all_query_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/get_all_query_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /get_all_query - tags: - - walker - /walker/get_all_query/{node}: - post: - operationId: api_entry_walker_get_all_query__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/get_all_query_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /get_all_query/{node} - tags: - - walker - /walker/get_no_body: - post: - operationId: api_root_walker_get_no_body_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /get_no_body - tags: - - walker - /walker/get_no_body/{node}: - post: - operationId: api_entry_walker_get_no_body__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /get_no_body/{node} - tags: - - walker - /walker/get_with_query: - post: - operationId: api_root_walker_get_with_query_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/get_with_query_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /get_with_query - tags: - - walker - /walker/get_with_query/{node}: - post: - operationId: api_entry_walker_get_with_query__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/get_with_query_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /get_with_query/{node} - tags: - - walker - /walker/populate_graph: - post: - operationId: api_root_walker_populate_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /populate_graph - tags: - - walker - /walker/populate_graph/{node}: - post: - operationId: api_entry_walker_populate_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /populate_graph/{node} - tags: - - walker - /walker/post_all_excluded: - post: - operationId: api_root_walker_post_all_excluded_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_all_excluded_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_all_excluded - tags: - - walker - /walker/post_all_excluded/{node}: - post: - operationId: api_entry_walker_post_all_excluded__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_all_excluded_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_all_excluded/{node} - tags: - - walker - /walker/post_no_body: - post: - operationId: api_root_walker_post_no_body_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /post_no_body - tags: - - walker - /walker/post_no_body/{node}: - post: - operationId: api_entry_walker_post_no_body__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_no_body/{node} - tags: - - walker - /walker/post_partial_excluded: - post: - operationId: api_root_walker_post_partial_excluded_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_partial_excluded_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_partial_excluded - tags: - - walker - /walker/post_partial_excluded/{node}: - post: - operationId: api_entry_walker_post_partial_excluded__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_partial_excluded_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_partial_excluded/{node} - tags: - - walker - /walker/post_path_var: - post: - operationId: api_root_walker_post_path_var_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_path_var_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_path_var - tags: - - walker - /walker/post_path_var/{node}: - post: - operationId: api_entry_walker_post_path_var__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_path_var_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_path_var/{node} - tags: - - walker - /walker/post_with_body: - post: - operationId: api_root_walker_post_with_body_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_with_body_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_with_body - tags: - - walker - /walker/post_with_body/{node}: - post: - operationId: api_entry_walker_post_with_body__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/post_with_body_body_model' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_with_body/{node} - tags: - - walker - /walker/post_with_body_and_file: - post: - operationId: api_root_walker_post_with_body_and_file_post - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/Body_api_root_walker_post_with_body_and_file_post' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_with_body_and_file - tags: - - walker - /walker/post_with_body_and_file/{node}: - post: - operationId: api_entry_walker_post_with_body_and_file__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/Body_api_entry_walker_post_with_body_and_file__node__post' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_with_body_and_file/{node} - tags: - - walker - /walker/post_with_file: - post: - operationId: api_root_walker_post_with_file_post - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/Body_api_root_walker_post_with_file_post' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_with_file - tags: - - walker - /walker/post_with_file/{node}: - post: - operationId: api_entry_walker_post_with_file__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/Body_api_entry_walker_post_with_file__node__post' - required: true - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /post_with_file/{node} - tags: - - walker - /walker/purge_populated_graph: - post: - operationId: api_root_walker_purge_populated_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /purge_populated_graph - tags: - - walker - /walker/purge_populated_graph/{node}: - post: - operationId: api_entry_walker_purge_populated_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /purge_populated_graph/{node} - tags: - - walker - /walker/traverse_graph: - post: - operationId: api_root_walker_traverse_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /traverse_graph - tags: - - walker - /walker/traverse_graph/{node}: - post: - operationId: api_entry_walker_traverse_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /traverse_graph/{node} - tags: - - walker - /walker/traverse_populated_graph: - post: - operationId: api_root_walker_traverse_populated_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /traverse_populated_graph - tags: - - walker - /walker/traverse_populated_graph/{node}: - post: - operationId: api_entry_walker_traverse_populated_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /traverse_populated_graph/{node} - tags: - - walker - /walker/update_graph: - post: - operationId: api_root_walker_update_graph_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /update_graph - tags: - - walker - /walker/update_graph/{node}: - post: - operationId: api_entry_walker_update_graph__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /update_graph/{node} - tags: - - walker - /walker/update_nested_node: - post: - operationId: api_root_walker_update_nested_node_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /update_nested_node - tags: - - walker - /walker/update_nested_node/{node}: - post: - operationId: api_entry_walker_update_nested_node__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /update_nested_node/{node} - tags: - - walker - /walker/visit_nested_node: - post: - operationId: api_root_walker_visit_nested_node_post - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - summary: /visit_nested_node - tags: - - walker - /walker/visit_nested_node/{node}: - post: - operationId: api_entry_walker_visit_nested_node__node__post - parameters: - - in: path - name: node - required: true - schema: - anyOf: - - type: string - - type: 'null' - title: Node - responses: - '200': - content: - application/json: - schema: {} - description: Successful Response - '422': - content: - application/json: - schema: - $ref: '#/components/schemas/HTTPValidationError' - description: Validation Error - summary: /visit_nested_node/{node} - tags: - - walker \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index e8baaae4fa..1a8fb913b6 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -5,8 +5,6 @@ import:py from jac_cloud.jaseci.utils {SendGridEmailer} import:py from dataclasses {dataclass} import:py from fastapi {UploadFile} -include:jac simple_graph_mini; - class User:BaseUser: { has name: str; @@ -45,65 +43,423 @@ class User:BaseUser: { } } +enum Enum { + A = "a", + B = "b", + C = "c" +} + +node A { + has val: int; +} + +node B { + has val: int; +} + +node C { + has val: int; +} -:walker:detach_node:can:enter_B { - return here del --> [-->]; +obj Child { + has val: int, arr: list[int], json: dict[str, int], enum_field: Enum; } -:walker:create_nested_node:can:enter_root { - 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 +obj Parent:Child: { + has child: Child; +} + +node Nested { + has val: int, arr: list[int], json: dict[str, int], 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 { + return 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.B - ), - enum_field = Enum.A - ); - here ++> n; - return n; -} - -:walker:update_nested_node:can:enter_root { - 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; - return nested; -} - -:walker:update_nested_node:can:enter_nested { - 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; - return here; + enum_field = Enum.A + ); + here ++> n; + return 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; + return 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; + return 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]; + Jac.destroy(nested); + # nested.__jac__.apply(); + + report [-->(`?Nested)]; + } +} + +walker delete_nested_edge { + can enter_root with `root entry { + nested_edge = :e:[-->][0]; + Jac.destroy(nested_edge); + + 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 { + Jac.unrestrict(here, self.level); + } else { + import:py from jac_cloud.core.architype {BaseAnchor} + Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); + } + } + + can enter_nested with Nested entry { + if self.via_all { + Jac.unrestrict(here, self.level); + } else { + import:py from jac_cloud.core.architype {BaseAnchor} + Jac.allow_root(here, BaseAnchor.ref(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 { + Jac.restrict(here); + } else { + import:py from jac_cloud.core.architype {BaseAnchor} + Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); + } + } + + can enter_nested with Nested entry { + if self.via_all { + Jac.restrict(here); + } else { + import:py from jac_cloud.core.architype {BaseAnchor} + Jac.disallow_root(here, BaseAnchor.ref(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 post_all_excluded { + has a: str = ""; + has b: str = ""; + + class __specs__ { + has excluded: str = "*", auth: bool = False; + } +} + +walker post_partial_excluded { + has a: str; + has b: str = ""; + + class __specs__ { + has excluded: str = "b"; + } +} + +walker combination1 { + has a: str; + has b: str; + has c: str; + has d: str = ""; + + class __specs__ { + has methods: list = ["post", "get"], as_query: list = ["a", "b"], excluded: str = "d"; + } +} + + +walker combination2 { + has a: str; + has b: str; + has c: str; + has d: str = ""; + has e: str = ""; + + class __specs__ { + has path: str = "/{a}", + methods: list = ["post", "get", "put", "patch", "delete", "head", "trace", "options"], + as_query: list = ["b"], + excluded: list = ["d", "e"]; + } +} + +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); + + multiple = []; + for file in self.multiple { + multiple.append({ + "name": file.filename, + "content_type": file.content_type, + "size": file.size, + }); + } + + single = { + "single": { + "name": self.single.filename, + "content_type": self.single.content_type, + "size": self.single.size, + } + }; + + report { + "single": single, + "multiple": multiple, + "singleOptional": 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; + } +} + +walker different_return { + + can enter1 with `root entry { + } + + can enter2 with A entry -> int { + } + + can enter3 with B entry -> str { + } + + can enter4 with C entry -> bool { + } + + can enter5 with Nested entry -> Parent { + } + + can enter6 with `root entry -> list | dict { + } + + class __specs__ { + has auth: bool = False; + } } :walker:detach_nested_node:can:enter_root { @@ -227,49 +583,131 @@ walker manual_delete_nested_edge { } } -:walker:allow_other_root_access:can:enter_root { - if self.via_all { - Jac.unrestrict(here, self.level); - } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); +walker custom_report { + can enter1 with `root entry { + report 1; + report 2; + report 3; + + Jac.report({ + "testing": 1 + }, True); + } + + class __specs__ { + has auth: bool = False; } } -:walker:allow_other_root_access:can:enter_nested { - if self.via_all { - Jac.unrestrict(here, self.level); - } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); +################################################################## +# FOR PURGER # +################################################################## + +node D { + has id: int; +} + +node E { + has id: int; +} + +node F { + has id: int; +} + +node G { + has id: int; +} + +node H { + has id: int; +} + + +walker populate_graph { + can setup1 with `root entry { + for i in range(2) { + here ++> D(id=i); + } + visit [-->]; + } + + can setup2 with D entry { + for i in range(2) { + here ++> E(id=i); + } + visit [-->]; + } + + can setup3 with E entry { + for i in range(2) { + here ++> F(id=i); + } + visit [-->]; + } + + can setup4 with F entry { + for i in range(2) { + here ++> G(id=i); + } + visit [-->]; + } + + can setup5 with G entry { + for i in range(2) { + here ++> H(id=i); + } + visit [-->]; } } -:walker:disallow_other_root_access:can:enter_root { - if self.via_all { - Jac.restrict(here); - } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); +walker traverse_populated_graph { + can enter1 with `root entry { + report here; + visit [-->]; + } + + can enter2 with D entry { + report here; + visit [-->]; + } + + can enter3 with E entry { + report here; + visit [-->]; + } + + can enter4 with F entry { + report here; + visit [-->]; + } + + can enter5 with G entry { + report here; + visit [-->]; + } + + can enter6 with H entry { + report here; + visit [-->]; } } -:walker:disallow_other_root_access:can:enter_nested { - if self.via_all { - Jac.restrict(here); - } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); +walker purge_populated_graph { + can purge with `root entry { + report Jac.reset_graph(); } } -:walker:check_populated_graph:can:enter { - import:py from jac_cloud.core.architype {NodeAnchor, EdgeAnchor, WalkerAnchor} +walker check_populated_graph { + can enter with `root entry { + import:py from jac_cloud.core.architype {NodeAnchor, EdgeAnchor, WalkerAnchor} - id = here.__jac__.id; - count = NodeAnchor.Collection.count({"$or": [{"_id": id}, {"root": id}]}); - count += EdgeAnchor.Collection.count({"root": id}); - count += WalkerAnchor.Collection.count({"root": id}); + id = here.__jac__.id; + count = NodeAnchor.Collection.count({"$or": [{"_id": id}, {"root": id}]}); + count += EdgeAnchor.Collection.count({"root": id}); + count += WalkerAnchor.Collection.count({"root": id}); - report count; + report count; + } } \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/simple_graph_mini.jac b/jac-cloud/jac_cloud/tests/simple_graph_mini.jac deleted file mode 100644 index 4ebe304453..0000000000 --- a/jac-cloud/jac_cloud/tests/simple_graph_mini.jac +++ /dev/null @@ -1,540 +0,0 @@ -"""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[int], json: dict[str, int], enum_field: Enum; -} - -obj Parent:Child: { - has child: Child; -} - -node Nested { - has val: int, arr: list[int], json: dict[str, int], 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]; - Jac.destroy(nested); - # nested.__jac__.apply(); - - report [-->(`?Nested)]; - } -} - -walker delete_nested_edge { - can enter_root with `root entry { - nested_edge = :e:[-->][0]; - Jac.destroy(nested_edge); - - 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 { - Jac.unrestrict(here, self.level); - } else { - Jac.allow_root(here, UUID(self.root_id), self.level); - } - } - - can enter_nested with Nested entry { - if self.via_all { - Jac.unrestrict(here, self.level); - } else { - Jac.allow_root(here, 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 { - Jac.restrict(here); - } else { - Jac.disallow_root(here, UUID(self.root_id)); - } - } - - can enter_nested with Nested entry { - if self.via_all { - Jac.restrict(here); - } else { - Jac.disallow_root(here, 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 post_all_excluded { - has a: str = ""; - has b: str = ""; - - class __specs__ { - has excluded: str = "*", auth: bool = False; - } -} - -walker post_partial_excluded { - has a: str; - has b: str = ""; - - class __specs__ { - has excluded: str = "b"; - } -} - -walker combination1 { - has a: str; - has b: str; - has c: str; - has d: str = ""; - - class __specs__ { - has methods: list = ["post", "get"], as_query: list = ["a", "b"], excluded: str = "d"; - } -} - - -walker combination2 { - has a: str; - has b: str; - has c: str; - has d: str = ""; - has e: str = ""; - - class __specs__ { - has path: str = "/{a}", - methods: list = ["post", "get", "put", "patch", "delete", "head", "trace", "options"], - as_query: list = ["b"], - excluded: list = ["d", "e"]; - } -} - -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); - - multiple = []; - for file in self.multiple { - multiple.append({ - "name": file.filename, - "content_type": file.content_type, - "size": file.size, - }); - } - - single = { - "single": { - "name": self.single.filename, - "content_type": self.single.content_type, - "size": self.single.size, - } - }; - - report { - "single": single, - "multiple": multiple, - "singleOptional": 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; - } -} - -walker different_return { - - can enter1 with `root entry { - } - - can enter2 with A entry -> int { - } - - can enter3 with B entry -> str { - } - - can enter4 with C entry -> bool { - } - - can enter5 with Nested entry -> Parent { - } - - can enter6 with `root entry -> list | dict { - } - - class __specs__ { - has auth: bool = False; - } -} - -walker custom_report { - can enter1 with `root entry { - report 1; - report 2; - report 3; - - Jac.report({ - "testing": 1 - }, True); - } - - class __specs__ { - has auth: bool = False; - } -} - -################################################################## -# FOR PURGER # -################################################################## - -node D { - has id: int; -} - -node E { - has id: int; -} - -node F { - has id: int; -} - -node G { - has id: int; -} - -node H { - has id: int; -} - - -walker populate_graph { - can setup1 with `root entry { - for i in range(2) { - here ++> D(id=i); - } - visit [-->]; - } - - can setup2 with D entry { - for i in range(2) { - here ++> E(id=i); - } - visit [-->]; - } - - can setup3 with E entry { - for i in range(2) { - here ++> F(id=i); - } - visit [-->]; - } - - can setup4 with F entry { - for i in range(2) { - here ++> G(id=i); - } - visit [-->]; - } - - can setup5 with G entry { - for i in range(2) { - here ++> H(id=i); - } - visit [-->]; - } -} - -walker traverse_populated_graph { - can enter1 with `root entry { - report here; - visit [-->]; - } - - can enter2 with D entry { - report here; - visit [-->]; - } - - can enter3 with E entry { - report here; - visit [-->]; - } - - can enter4 with F entry { - report here; - visit [-->]; - } - - can enter5 with G entry { - report here; - visit [-->]; - } - - can enter6 with H entry { - report here; - visit [-->]; - } -} - -walker purge_populated_graph { - can purge with `root entry { - report Jac.reset_graph(); - } -} - -walker check_populated_graph { - can enter with `root entry { - report len(Jac.get_context().mem.__shelf__.values()); - } -} \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/test.json b/jac-cloud/jac_cloud/tests/test.json deleted file mode 100644 index e9bdb8309b..0000000000 --- a/jac-cloud/jac_cloud/tests/test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "testing": 1 -} \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph.py b/jac-cloud/jac_cloud/tests/test_simple_graph.py index e848f77161..91ac9111f8 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph.py @@ -1,82 +1,30 @@ """JacLang Jaseci Unit Test.""" -from contextlib import suppress -from os import getenv -from typing import Literal, overload -from unittest.async_case import IsolatedAsyncioTestCase - from httpx import get, post from yaml import safe_load +from .test_utils import JacCloudTest from ..jaseci.datasources import Collection -class SimpleGraphTest(IsolatedAsyncioTestCase): +class SimpleGraphTest(JacCloudTest): """JacLang Jaseci Feature Tests.""" - async def asyncSetUp(self) -> None: - """Reset DB and wait for server.""" - self.host = "http://0.0.0.0:8000" + def setUp(self) -> None: + """Override setUp.""" + self.run_server("jac_cloud/tests/simple_graph.jac") + Collection.__client__ = None Collection.__database__ = None self.client = Collection.get_client() self.q_node = Collection.get_collection("node") self.q_edge = Collection.get_collection("edge") - self.users: list[dict] = [] - self.database = getenv("DATABASE_NAME", "jaseci") - 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.client.drop_database(self.database) - - @overload - def post_api(self, api: str, json: dict | None = None, user: int = 0) -> dict: - pass - - @overload - def post_api( - self, - api: str, - json: dict | None = None, - user: int = 0, - expect_error: Literal[True] = True, - ) -> int: - pass - - def post_api( - self, - api: str, - json: dict | None = None, - user: int = 0, - expect_error: bool = False, - ) -> dict | int: - """Call walker post API.""" - res = post( - f"{self.host}/walker/{api}", json=json, headers=self.users[user]["headers"] - ) - - 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}/healthz") - res.raise_for_status() - self.assertEqual(200, res.status_code) + def tearDown(self) -> None: + """Override tearDown.""" + self.client.drop_database(self.database) + self.stop_server() def trigger_openapi_specs_test(self) -> None: """Test OpenAPI Specs.""" @@ -612,19 +560,19 @@ def trigger_upload_file(self) -> None: "single": { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 7113, + "size": 15146, } }, "multiple": [ { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 7113, + "size": 15146, }, { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 7113, + "size": 15146, }, ], "singleOptional": None, @@ -672,7 +620,7 @@ def trigger_reset_graph(self) -> None: self.assertEqual([None], res["returns"]) self.assertEqual([1], res["reports"]) - async def test_all_features(self) -> None: + def test_all_features(self) -> None: """Test Full Features.""" self.trigger_openapi_specs_test() diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py b/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py deleted file mode 100644 index 42d74f88b0..0000000000 --- a/jac-cloud/jac_cloud/tests/test_simple_graph_mini.py +++ /dev/null @@ -1,508 +0,0 @@ -"""JacLang Jaseci Unit Test.""" - -from contextlib import suppress -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 - -from yaml import safe_load - - -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}/walker/{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}/healthz") - 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.yaml", timeout=1) - res.raise_for_status() - - with open("jac_cloud/tests/openapi_specs_mini.yaml") as file: - self.assertEqual(safe_load(file), safe_load(res.text)) - - 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"]) - - 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() - - 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} - ) - - def trigger_custom_report(self) -> None: - """Test custom status code.""" - res = self.post_api("custom_report") - self.assertEqual({"testing": 1}, res) - - def trigger_upload_file(self) -> None: - """Test upload file.""" - with open("jac_cloud/tests/simple_graph.jac", mode="br") as s: - files = [ - ("single", ("simple_graph.jac", s)), - ("multiple", ("simple_graph.jac", s)), - ("multiple", ("simple_graph.jac", s)), - ] - res = post(f"{self.host}/walker/post_with_file", files=files) - res.raise_for_status() - data: dict = res.json() - - self.assertEqual(200, data["status"]) - self.assertEqual( - [ - { - "single": { - "single": { - "name": "simple_graph.jac", - "content_type": "application/octet-stream", - "size": 7113, - } - }, - "multiple": [ - { - "name": "simple_graph.jac", - "content_type": "application/octet-stream", - "size": 7113, - }, - { - "name": "simple_graph.jac", - "content_type": "application/octet-stream", - "size": 7113, - }, - ], - "singleOptional": None, - } - ], - data["reports"], - ) - - def trigger_reset_graph(self) -> None: - """Test custom status code.""" - res = self.post_api("populate_graph") - self.assertEqual(200, res["status"]) - - res = self.post_api("traverse_populated_graph") - self.assertEqual(200, res["status"]) - reports = res["reports"] - - root = reports.pop(0) - self.assertEqual({}, root["context"]) - - for idx in range(0, 62): - self.assertEqual({"id": idx % 2}, reports[idx]["context"]) - - res = self.post_api("check_populated_graph") - self.assertEqual(200, res["status"]) - self.assertEqual([125], res["reports"]) - - res = self.post_api("purge_populated_graph") - self.assertEqual(200, res["status"]) - self.assertEqual([124], res["reports"]) - - res = self.post_api("check_populated_graph") - self.assertEqual(200, res["status"]) - self.assertEqual([1], res["reports"]) - - async def test_all_features(self) -> None: - """Test Full Features.""" - self.trigger_openapi_specs_test() - - 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() - - self.nested_count_should_be(node=0, edge=0) - - self.trigger_create_nested_node_test() - self.nested_count_should_be(node=1, edge=1) - - self.trigger_update_nested_node_test() - self.trigger_detach_nested_node_test() - self.nested_count_should_be(node=0, edge=0) - - ################################################### - # VIA DESTROY # - ################################################### - - self.trigger_create_nested_node_test() - self.nested_count_should_be(node=1, edge=1) - - self.trigger_delete_nested_node_test() - self.nested_count_should_be(node=0, edge=0) - - self.trigger_create_nested_node_test() - self.nested_count_should_be(node=1, edge=1) - - self.trigger_delete_nested_edge_test() - self.nested_count_should_be(node=0, edge=0) - - ################################################### - # CUSTOM STATUS # - ################################################### - - self.trigger_custom_status_code() - - ################################################### - # CUSTOM REPORT # - ################################################### - - self.trigger_custom_report() - - ################################################### - # FILE UPLOAD # - ################################################### - - self.trigger_upload_file() - - ################################################### - # TEST PURGER # - ################################################### - - self.trigger_reset_graph() diff --git a/jac-cloud/jac_cloud/tests/test_utils.py b/jac-cloud/jac_cloud/tests/test_utils.py new file mode 100644 index 0000000000..6f50ecff2f --- /dev/null +++ b/jac-cloud/jac_cloud/tests/test_utils.py @@ -0,0 +1,114 @@ +"""Test utils.""" + +from contextlib import suppress +from os import environ, getenv +from subprocess import Popen, run +from typing import Literal, overload +from unittest import TestCase + +from fakeredis import FakeRedis + +from httpx import get, post + +from pymongo import MongoClient + +from redis import Redis as RedisClient + +from ..jaseci.datasources import Collection, MontyClient, Redis + + +class JacCloudTest(TestCase): + """Test Utils.""" + + def run_server( + self, + file: str, + port: int = 8000, + database: str = "jaseci", + envs: dict | None = None, + wait: int = 5, + mini: bool = False, + ) -> None: + """Run server.""" + run(["fuser", "-k", f"{port}/tcp"]) + run(["jac", "clean"]) + run(["jac", "tool", "gen_parser"]) + + base_envs = environ.copy() + base_envs["DATABASE_NAME"] = database + base_envs.update(envs or {"DATABASE_NAME": database}) + + self.server = Popen( + ["jac", "serve", f"{file}", "--port", f"{port}"], env=base_envs + ) + + run(["sleep", f"{wait}"]) + + self.host = f"http://0.0.0.0:{port}" + self.database = database + self.users: list[dict] = [] + + self.root_id_prefix = "" if mini else "n::" + + count = 0 + while True: + if count > 5: + self.check_server() + break + else: + with suppress(Exception): + self.check_server() + break + count += 1 + + def stop_server(self) -> None: + """Stop server.""" + self.server.kill() + + def check_server(self) -> None: + """Retrieve OpenAPI Specs JSON.""" + res = get(f"{self.host}/healthz") + res.raise_for_status() + self.assertEqual(200, res.status_code) + + if getenv("DATABASE_HOST"): + self.assertIsInstance(Collection.get_client(), MongoClient) + else: + self.assertIsInstance(Collection.get_client(), MontyClient) + + if getenv("REDIS_HOST"): + self.assertIsInstance(Redis.get_rd(), RedisClient) + else: + self.assertIsInstance(Redis.get_rd(), FakeRedis) + + @overload + def post_api(self, api: str, json: dict | None = None, user: int = 0) -> dict: + pass + + @overload + def post_api( + self, + api: str, + json: dict | None = None, + user: int = 0, + expect_error: Literal[True] = True, + ) -> int: + pass + + def post_api( + self, + api: str, + json: dict | None = None, + user: int = 0, + expect_error: bool = False, + ) -> dict | int: + """Call walker post API.""" + res = post( + f"{self.host}/walker/{api}", json=json, headers=self.users[user]["headers"] + ) + + if not expect_error: + res.raise_for_status() + return res.json() + else: + return res.status_code diff --git a/jac-cloud/setup.py b/jac-cloud/setup.py index 342229000a..d14fc9886b 100644 --- a/jac-cloud/setup.py +++ b/jac-cloud/setup.py @@ -33,6 +33,7 @@ "fakeredis~=2.24.1", "ecs-logging~=2.2.0", "types-PyYAML~=6.0.12.20240917", + "montydb~=2.5.3", ], package_data={}, entry_points={