import ast import logging from typing import List from prisma.models import ( APIRouteSpec, CompiledRoute, CompletedApp, Function, Package, Specification, ) from prisma.types import CompiledRouteCreateInput, CompletedAppCreateInput from pydantic import BaseModel from codex.api_model import Identifiers logger = logging.getLogger(__name__) class CompiledFunction(BaseModel): packages: List[Package] imports: List[str] code: str async def compile_route( ids: Identifiers, route_root_func: Function, api_route: APIRouteSpec ) -> CompiledRoute: """ Compiles a route by generating a CompiledRoute object. Args: ids (Identifiers): The identifiers used in the route. route_root_func (Function): The root function of the route. api_route (APIRouteSpec): The specification of the API route. Returns: CompiledRoute: The compiled route object. """ compiled_function = await recursive_compile_route(route_root_func) unique_packages = list(set([package.id for package in compiled_function.packages])) code = "\n".join(compiled_function.imports) code += "\n\n" code += compiled_function.code data = CompiledRouteCreateInput( description=api_route.description, Packages={"connect": [{"id": package_id} for package_id in unique_packages]}, fileName=api_route.functionName + "_service.py", mainFunctionName=route_root_func.functionName, compiledCode=code, RootFunction={"connect": {"id": route_root_func.id}}, ApiRouteSpec={"connect": {"id": api_route.id}}, ) compiled_route = await CompiledRoute.prisma().create(data) return compiled_route async def recursive_compile_route(in_function: Function) -> CompiledFunction: """ Recursively compiles a function and its child functions into a single CompiledFunction object. Args: ids (Identifiers): The identifiers for the function. function (Function): The function to compile. Returns: CompiledFunction: The compiled function. Raises: ValueError: If the function code is missing. """ # Can't see how to do recursive lookup with prisma, so I'm checking the next # layer down each time. This is a bit of a hack, can be improved later. function = await Function.prisma().find_unique_or_raise( where={"id": in_function.id}, include={ "ParentFunction": True, "ChildFunction": {"include": {"ApiRouteSpec": True}}, }, ) logger.info(f"Compiling function: {function.id}") if function.ChildFunction is None: packages = [] if function.Packages: packages = function.Packages if function.functionCode is None: raise ValueError(f"Leaf Function code is required! {function.id}") code = "\n".join(function.importStatements) code += "\n\n" code += function.functionCode try: tree = ast.parse(code) except Exception as e: raise ValueError(f"Syntax error in function code: {e}, {code}") return CompiledFunction( packages=packages, imports=function.importStatements, code=function.functionCode, ) else: packages = [] imports = [] code = "" for child_function in function.ChildFunction: compiled_function = await recursive_compile_route(child_function) packages.extend(compiled_function.packages) imports.extend(compiled_function.imports) code += "\n\n" code += compiled_function.code if function.Packages: packages.extend(function.Packages) imports.extend(function.importStatements) if function.functionCode is None: raise ValueError(f"Function code is required! {function.id}") code += "\n\n" code += function.functionCode check_code = "\n".join(imports) check_code += "\n\n" check_code += code try: tree = ast.parse(check_code) except Exception as e: raise ValueError(f"Syntax error in function code: {e}, {code}") return CompiledFunction(packages=packages, imports=imports, code=code) async def create_app( ids: Identifiers, spec: Specification, compiled_routes: List[CompiledRoute] ) -> CompletedApp: """ Create an app using the given identifiers, specification, and compiled routes. Args: ids (Identifiers): The identifiers for the app. spec (Specification): The specification for the app. compiled_routes (List[CompiledRoute]): The compiled routes for the app. Returns: CompletedApp: The completed app object. """ if spec.ApiRouteSpecs is None: raise ValueError("Specification must have at least one API route.") data = CompletedAppCreateInput( name=spec.name, description=spec.context, User={"connect": {"id": ids.user_id}}, CompiledRoutes={"connect": [{"id": route.id} for route in compiled_routes]}, Specification={"connect": {"id": spec.id}}, Application={"connect": {"id": ids.app_id}}, ) app = await CompletedApp.prisma().create(data) return app