Exemple #1
0
def get_base(
    types_dict: TypesDict,
    tmp_dir: str,
    scene_object_types: Set[str],
    obj_type: ObjectType,
    zf: zipfile.ZipFile,
    ot_path: str,
) -> None:

    base = base_from_source(obj_type.source, obj_type.id)

    if base is None:
        raise Arcor2Exception(
            f"Could not determine base class for {obj_type.id}.")

    if base in types_dict.keys() | built_in_types_names() | scene_object_types:
        return

    logger.debug(f"Getting {base} as base of {obj_type.id}.")
    base_obj_type = ps.get_object_type(base)

    zf.writestr(
        os.path.join(ot_path, humps.depascalize(base_obj_type.id)) + ".py",
        base_obj_type.source)

    types_dict[base_obj_type.id] = save_and_import_type_def(
        base_obj_type.source, base_obj_type.id, Generic, tmp_dir,
        OBJECT_TYPE_MODULE)

    # try to get base of the base
    get_base(types_dict, tmp_dir, scene_object_types, base_obj_type, zf,
             ot_path)
Exemple #2
0
def get_base_from_project_service(
    types_dict: TypesDict,
    tmp_dir: str,
    scene_object_types: set[str],
    obj_type: ObjectType,
    zf: zipfile.ZipFile,
    ot_path: str,
    ast: ast.AST,
) -> None:

    for idx, base in enumerate(base_from_source(ast, obj_type.id)):

        if base in types_dict.keys() | built_in_types_names(
        ) | scene_object_types:
            continue

        logger.debug(f"Getting {base} as base of {obj_type.id}.")
        base_obj_type = ps.get_object_type(base)

        # first try if the code is valid
        try:
            base_ast = parse(base_obj_type.source)
        except Arcor2Exception:
            raise FlaskException(
                f"Invalid code of the {base_obj_type.id} (base of {obj_type.id}).",
                error_code=401)

        # try to get base of the base
        get_base_from_project_service(types_dict, tmp_dir, scene_object_types,
                                      base_obj_type, zf, ot_path, base_ast)

        if idx == 0:  # this is the base ObjectType
            types_dict[base_obj_type.id] = save_and_import_type_def(
                base_obj_type.source, base_obj_type.id, Generic, tmp_dir,
                OBJECT_TYPE_MODULE)
        else:  # these are potential mixins (just try to import them, no need to store them)
            save_and_import_type_def(base_obj_type.source, base_obj_type.id,
                                     object, tmp_dir, OBJECT_TYPE_MODULE)
            scene_object_types.add(base_obj_type.id)

        zf.writestr(
            os.path.join(ot_path, humps.depascalize(base_obj_type.id)) + ".py",
            base_obj_type.source)
Exemple #3
0
def get_base_from_imported_package(obj_type: ObjectType,
                                   types_dict: dict[str, ObjectType],
                                   zip_file: zipfile.ZipFile, tmp_dir: str,
                                   ast: ast.AST) -> None:

    for idx, base in enumerate(base_from_source(ast, obj_type.id)):

        if base in types_dict.keys() | built_in_types_names():
            continue

        logger.debug(f"Getting {base} as base of {obj_type.id}.")

        try:
            base_obj_type_src = read_str_from_zip(
                zip_file, f"object_types/{humps.depascalize(base)}.py")
        except KeyError:
            raise FlaskException(
                f"Could not find {base} object type (base of {obj_type.id}).",
                error_code=401)

        # first try if the code is valid
        try:
            base_ast = parse(base_obj_type_src)
        except Arcor2Exception:
            raise FlaskException(
                f"Invalid code of the {base} (base of {obj_type.id}).",
                error_code=401)

        types_dict[base] = ObjectType(base, base_obj_type_src)

        # try to get base of the base
        get_base_from_imported_package(types_dict[base], types_dict, zip_file,
                                       tmp_dir, base_ast)

        # then, try to import it (no need to store the result)
        if idx == 0:  # this is the base ObjectType
            save_and_import_type_def(base_obj_type_src, base, Generic, tmp_dir,
                                     OBJECT_TYPE_MODULE)
        else:  # these are potential mixins
            save_and_import_type_def(base_obj_type_src, base, object, tmp_dir,
                                     OBJECT_TYPE_MODULE)
Exemple #4
0
def project_import() -> RespT:
    """Imports a project from execution package.
    ---
    put:
      tags:
        - Build
      summary: Imports a project from execution package.
      operationId: ProjectImport
      parameters:
            - in: query
              name: overwriteScene
              schema:
                type: boolean
                default: false
              description: overwrite Scene
            - in: query
              name: overwriteProject
              schema:
                type: boolean
                default: false
              description: overwrite Project
            - in: query
              name: overwriteObjectTypes
              schema:
                type: boolean
                default: false
              description: overwrite ObjectTypes
            - in: query
              name: overwriteProjectSources
              schema:
                type: boolean
                default: false
              description: overwrite ProjectSources
            - in: query
              name: overwriteCollisionModels
              schema:
                type: boolean
                default: false
              description: overwrite collision models
      requestBody:
              content:
                multipart/form-data:
                  schema:
                    type: object
                    required:
                        - executionPackage
                    properties:
                      executionPackage:
                        type: string
                        format: binary
      responses:
        200:
          description: Ok
          content:
                application/json:
                  schema:
                    $ref: ImportResult
        400:
          description: Some other error occurred.
          content:
                application/json:
                  schema:
                    type: string
        401:
          description: Invalid execution package.
          content:
                application/json:
                  schema:
                    type: string
        402:
          description: A difference between package/project service detected (overwrite needed).
          content:
                application/json:
                  schema:
                    type: string
        404:
          description: Something is missing.
          content:
                application/json:
                  schema:
                    type: string
    """

    file = request.files["executionPackage"]

    overwrite_scene = request.args.get("overwriteScene",
                                       default="false") == "true"
    overwrite_project = request.args.get("overwriteProject",
                                         default="false") == "true"
    overwrite_object_types = request.args.get("overwriteObjectTypes",
                                              default="false") == "true"
    overwrite_project_sources = request.args.get("overwriteProjectSources",
                                                 default="false") == "true"
    overwrite_collision_models = request.args.get("overwriteCollisionModels",
                                                  default="false") == "true"

    objects: dict[str, ObjectType] = {}
    models: dict[str, Models] = {}
    """
    1) get and validate all data from zip
    2) check what is already on the Project service
    3) do updates
    """
    # BytesIO + stream.read() = workaround for a Python bug (SpooledTemporaryFile not seekable)
    with zipfile.ZipFile(BytesIO(file.stream.read())) as zip_file:

        try:
            project = read_dc_from_zip(zip_file, "data/project.json", Project)
        except KeyError:
            raise FlaskException("Could not find project.json.",
                                 error_code=404)
        except (json.JsonException, ValidationError) as e:
            raise FlaskException(f"Failed to process project.json. {str(e)}",
                                 error_code=401)

        try:
            scene = read_dc_from_zip(zip_file, "data/scene.json", Scene)
        except KeyError:
            raise FlaskException("Could not find scene.json.", error_code=404)
        except (json.JsonException, ValidationError) as e:
            return json.dumps(f"Failed to process scene.json. {str(e)}"), 401

        if project.scene_id != scene.id:
            raise FlaskException("Project assigned to different scene id.",
                                 error_code=401)

        with tempfile.TemporaryDirectory() as tmp_dir:

            # restore original environment
            sys.path = list(original_sys_path)
            sys.modules = dict(original_sys_modules)

            prepare_object_types_dir(tmp_dir, OBJECT_TYPE_MODULE)

            for scene_obj in scene.objects:

                obj_type_name = scene_obj.type

                if obj_type_name in objects:  # there might be more instances of the same type
                    continue

                logger.debug(f"Importing {obj_type_name}.")

                try:
                    obj_type_src = read_str_from_zip(
                        zip_file,
                        f"object_types/{humps.depascalize(obj_type_name)}.py")
                except KeyError:
                    raise FlaskException(
                        f"Object type {obj_type_name} is missing in the package.",
                        error_code=404)

                try:
                    ast = parse(obj_type_src)
                except Arcor2Exception:
                    raise FlaskException(
                        f"Invalid code of the {obj_type_name} object type.",
                        error_code=401)

                # TODO fill in OT description (is it used somewhere?)
                objects[obj_type_name] = ObjectType(obj_type_name,
                                                    obj_type_src)
                get_base_from_imported_package(objects[obj_type_name], objects,
                                               zip_file, tmp_dir, ast)

                type_def = save_and_import_type_def(obj_type_src,
                                                    obj_type_name, Generic,
                                                    tmp_dir,
                                                    OBJECT_TYPE_MODULE)

                assert obj_type_name == type_def.__name__

                if type_def.abstract():
                    raise FlaskException(
                        f"Scene contains abstract object type: {obj_type_name}.",
                        error_code=401)

        for obj_type in objects.values():  # handle models

            # TODO rather iterate on content of data/models?
            try:
                model = read_dc_from_zip(
                    zip_file,
                    f"data/models/{humps.depascalize(obj_type.id)}.json",
                    ObjectModel).model()
            except KeyError:
                continue

            logger.debug(f"Found model {model.id} of type {model.type}.")

            obj_type.model = model.metamodel()

            if obj_type.id != obj_type.model.id:
                raise FlaskException(
                    f"Model id ({obj_type.model.id}) has to be the same as ObjectType id ({obj_type.id}).",
                    error_code=401,
                )

            models[obj_type.id] = model

        if not project.has_logic:
            logger.debug("Importing the main script.")

            try:
                script = zip_file.read("script.py").decode("UTF-8")
            except KeyError:
                raise FlaskException("Could not find script.py.",
                                     error_code=404)

            try:
                parse(script)
            except Arcor2Exception:
                raise FlaskException("Invalid code of the main script.",
                                     error_code=401)

    # check that we are not going to overwrite something
    if not overwrite_scene:

        try:
            ps_scene = ps.get_scene(scene.id)
        except ps.ProjectServiceException:
            pass
        else:

            # do not take created / modified into account
            ps_scene.created = scene.created = None
            ps_scene.modified = scene.modified = None

            if ps_scene != scene:
                raise FlaskException(
                    "Scene difference detected. Overwrite needed.",
                    error_code=402)

    if not overwrite_project:

        try:
            ps_project = ps.get_project(project.id)
        except ps.ProjectServiceException:
            pass
        else:

            # do not take created / modified into account
            ps_project.created = project.created = None
            ps_project.modified = project.modified = None

            if ps_project != project:
                raise FlaskException(
                    "Project difference detected. Overwrite needed.",
                    error_code=402)

    if not overwrite_object_types:

        for obj_type in objects.values():

            try:

                ot = ps.get_object_type(obj_type.id)

                # ignore changes in description (no one cares)
                if ot.source != obj_type.source or ot.model != obj_type.model:
                    raise FlaskException(
                        f"Difference detected for {obj_type.id} object type. Overwrite needed.",
                        error_code=402)
            except ps.ProjectServiceException:
                pass

    if not overwrite_project_sources and not project.has_logic:

        try:
            if ps.get_project_sources(project.id).script != script:
                raise FlaskException(
                    "Script difference detected. Overwrite needed.",
                    error_code=402)
        except ps.ProjectServiceException:
            pass

    if not overwrite_collision_models:

        for model in models.values():
            try:
                if model != ps.get_model(model.id, model.type()):
                    raise FlaskException(
                        "Collision model difference detected. Overwrite needed.",
                        error_code=402)
            except ps.ProjectServiceException:
                pass

    for model in models.values():
        ps.put_model(model)

    for obj_type in objects.values():
        ps.update_object_type(obj_type)

    ps.update_scene(scene)
    ps.update_project(project)
    if not project.has_logic:
        ps.update_project_sources(ProjectSources(project.id, script))

    logger.info(
        f"Imported project {project.name} (scene {scene.name}), with {len(objects)} "
        f"object type(s) and {len(models)} model(s).")

    return ImportResult(scene.id, project.id).to_json(), 200
Exemple #5
0
def _publish(project_id: str, package_name: str) -> RespT:

    mem_zip = BytesIO()

    logger.debug(
        f"Generating package {package_name} for project_id: {project_id}.")

    types_dict: TypesDict = {}

    # restore original environment
    sys.path = list(original_sys_path)
    sys.modules = dict(original_sys_modules)

    with tempfile.TemporaryDirectory() as tmp_dir:

        prepare_object_types_dir(tmp_dir, OBJECT_TYPE_MODULE)

        with zipfile.ZipFile(mem_zip,
                             mode="w",
                             compression=zipfile.ZIP_DEFLATED) as zf:

            try:
                logger.debug("Getting scene and project.")
                project = ps.get_project(project_id)
                cached_project = CachedProject(project)
                scene = ps.get_scene(project.scene_id)
                cached_scene = CachedScene(scene)

                if not package_name:
                    package_name = project.name

                data_path = "data"
                ot_path = "object_types"

                zf.writestr(os.path.join(ot_path, "__init__.py"), "")
                zf.writestr(os.path.join(data_path, "project.json"),
                            project.to_json())
                zf.writestr(os.path.join(data_path, "scene.json"),
                            scene.to_json())

                obj_types = set(cached_scene.object_types)
                obj_types_with_models: set[str] = set()

                if __debug__:  # this should uncover potential problems with order in which ObjectTypes are processed
                    import random

                    random.shuffle(scene.objects)

                for scene_obj in scene.objects:

                    if scene_obj.type in types_dict:
                        continue

                    logger.debug(
                        f"Getting scene object type {scene_obj.type}.")
                    obj_type = ps.get_object_type(scene_obj.type)

                    if obj_type.model and obj_type.id not in obj_types_with_models:
                        obj_types_with_models.add(obj_type.id)

                        model = ps.get_model(obj_type.model.id,
                                             obj_type.model.type)
                        obj_model = ObjectModel(
                            obj_type.model.type,
                            **{model.type().value.lower():
                               model}  # type: ignore
                        )

                        zf.writestr(
                            os.path.join(
                                data_path, "models",
                                humps.depascalize(obj_type.id) + ".json"),
                            obj_model.to_json(),
                        )

                    zf.writestr(
                        os.path.join(ot_path, humps.depascalize(obj_type.id)) +
                        ".py", obj_type.source)

                    # handle inheritance
                    get_base_from_project_service(types_dict, tmp_dir,
                                                  obj_types, obj_type, zf,
                                                  ot_path,
                                                  parse(obj_type.source))

                    types_dict[scene_obj.type] = save_and_import_type_def(
                        obj_type.source, scene_obj.type, Generic, tmp_dir,
                        OBJECT_TYPE_MODULE)

            except Arcor2Exception as e:
                logger.exception(
                    f"Failed to prepare package content. {str(e)}")
                raise FlaskException(str(e), error_code=404)

            script_path = "script.py"

            try:

                if project.has_logic:
                    logger.debug("Generating script from project logic.")
                    zf.writestr(
                        script_path,
                        program_src(types_dict, cached_project, cached_scene,
                                    True))
                else:
                    try:
                        logger.debug("Getting project sources.")
                        script = ps.get_project_sources(project.id).script

                        # check if it is a valid Python code
                        try:
                            parse(script)
                        except SourceException:
                            logger.exception(
                                "Failed to parse code of the uploaded script.")
                            raise FlaskException("Invalid code.",
                                                 error_code=501)

                        zf.writestr(script_path, script)

                    except ps.ProjectServiceException:

                        logger.info(
                            "Script not found on project service, creating one from scratch."
                        )

                        # write script without the main loop
                        zf.writestr(
                            script_path,
                            program_src(types_dict, cached_project,
                                        cached_scene, False))

                logger.debug("Generating supplementary files.")

                logger.debug("action_points.py")
                zf.writestr("action_points.py",
                            global_action_points_class(cached_project))

                logger.debug("package.json")
                zf.writestr(
                    "package.json",
                    PackageMeta(package_name,
                                datetime.now(tz=timezone.utc)).to_json())

            except Arcor2Exception as e:
                logger.exception("Failed to generate script.")
                raise FlaskException(str(e), error_code=501)

    logger.info(
        f"Done with {package_name} (scene {scene.name}, project {project.name})."
    )
    mem_zip.seek(0)

    return send_file(mem_zip,
                     as_attachment=True,
                     max_age=0,
                     download_name=f"{package_name}_package.zip")