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
def handle_dobot_exception(e: DobotApiException) -> tuple[str, int]: return json.dumps(str(e)), 400
def value_to_json(cls, value: Any) -> str: return json.dumps(value)
def test_project_const(start_processes: None, ars: ARServer) -> None: event(ars, events.c.ShowMainScreen) assert ars.call_rpc( rpc.s.NewScene.Request(uid(), rpc.s.NewScene.Request.Args("Test scene")), rpc.s.NewScene.Response).result scene_data = event(ars, events.s.OpenScene).data assert scene_data scene = scene_data.scene event(ars, events.s.SceneState) assert ars.call_rpc( rpc.s.AddObjectToScene.Request( uid(), rpc.s.AddObjectToScene.Request.Args("random_actions", RandomActions.__name__)), rpc.s.AddObjectToScene.Response, ).result obj = event(ars, events.s.SceneObjectChanged).data assert obj # ------------------------------------------------------------------------------------------------------------------ assert ars.call_rpc( rpc.p.NewProject.Request( uid(), rpc.p.NewProject.Request.Args(scene.id, "Project name")), rpc.p.NewProject.Response, ).result proj = event(ars, events.p.OpenProject).data assert proj event(ars, events.s.SceneState) assert ars.call_rpc( rpc.p.AddConstant.Request( uid(), rpc.p.AddConstant.Request.Args("min_time", "double", json.dumps(0.45))), rpc.p.AddConstant.Response, ).result c1 = event(ars, events.p.ProjectConstantChanged).data assert c1 assert not ars.call_rpc( rpc.p.AddConstant.Request( uid(), rpc.p.AddConstant.Request.Args("min_time", "double", json.dumps(0.62))), rpc.p.AddConstant.Response, ).result assert not ars.call_rpc( # attempt to update without lock rpc.p.UpdateConstant.Request( uid(), rpc.p.UpdateConstant.Request.Args(c1.id, name="min_time_updated")), rpc.p.UpdateConstant.Response, ).result # ------------------------------------------------------------------------------------------------------------------ # the user opens a menu and then closes it without actually changing anything lock_object(ars, c1.id) assert ars.call_rpc( rpc.p.UpdateConstant.Request(uid(), rpc.p.UpdateConstant.Request.Args( c1.id, name="min_time_1"), dry_run=True), rpc.p.UpdateConstant.Response, ).result assert ars.call_rpc( rpc.p.UpdateConstant.Request(uid(), rpc.p.UpdateConstant.Request.Args( c1.id, name="min_time_2"), dry_run=True), rpc.p.UpdateConstant.Response, ).result unlock_object(ars, c1.id) # ------------------------------------------------------------------------------------------------------------------ lock_object(ars, c1.id) assert ars.call_rpc( rpc.p.UpdateConstant.Request( uid(), rpc.p.UpdateConstant.Request.Args(c1.id, name="min_time_updated")), rpc.p.UpdateConstant.Response, ).result c1u = event(ars, events.p.ProjectConstantChanged).data assert c1u event(ars, events.lk.ObjectsUnlocked) assert c1u.id == c1.id assert c1.name != c1u.name assert c1.type == c1u.type # ------------------------------------------------------------------------------------------------------------------ # try to add and remove assert ars.call_rpc( rpc.p.AddConstant.Request( uid(), rpc.p.AddConstant.Request.Args("min_time_2", "double", json.dumps(0.62))), rpc.p.AddConstant.Response, ).result c2 = event(ars, events.p.ProjectConstantChanged).data assert c2 assert ars.call_rpc( rpc.p.RemoveConstant.Request(uid(), rpc.p.RemoveConstant.Request.Args(c2.id)), rpc.p.RemoveConstant.Response, ).result c2e = event(ars, events.p.ProjectConstantChanged) assert c2e.data assert c2e.data.id == c2.id assert c2e.change_type == c2e.Type.REMOVE # ------------------------------------------------------------------------------------------------------------------ # attempt to add a constant with duplicate name assert not ars.call_rpc( rpc.p.AddConstant.Request( uid(), rpc.p.AddConstant.Request.Args(c1u.name, "double", json.dumps(0.62))), rpc.p.AddConstant.Response, ).result # ------------------------------------------------------------------------------------------------------------------ assert ars.call_rpc( rpc.p.AddActionPoint.Request( uid(), rpc.p.AddActionPoint.Request.Args("ap1", common.Position())), rpc.p.AddActionPoint.Response, ).result ap = event(ars, events.p.ActionPointChanged).data assert ap is not None assert ars.call_rpc( rpc.p.AddAction.Request( uid(), rpc.p.AddAction.Request.Args( ap.id, "test_action", f"{obj.id}/{RandomActions.random_double.__name__}", [ common.ActionParameter( "range_min", common.ActionParameter.TypeEnum.CONSTANT, json.dumps(c1.id)), common.ActionParameter("range_max", "double", "0.55"), ], [common.Flow(outputs=["random_value"])], ), ), rpc.p.AddAction.Response, ).result action = event(ars, events.p.ActionChanged).data assert action assert not ars.call_rpc( rpc.p.RemoveConstant.Request(uid(), rpc.p.RemoveConstant.Request.Args(c1.id)), rpc.p.RemoveConstant.Response).result # ------------------------------------------------------------------------------------------------------------------ # try to execute action using constant parameter assert ars.call_rpc((rpc.s.StartScene.Request(uid())), rpc.s.StartScene.Response).result assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Starting assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Started assert ars.call_rpc( rpc.p.ExecuteAction.Request( uid(), rpc.p.ExecuteAction.Request.Args(action.id)), rpc.p.ExecuteAction.Response) event(ars, events.a.ActionExecution) res = event(ars, events.a.ActionResult) assert res.data assert res.data.action_id == action.id assert not res.data.error assert ars.call_rpc((rpc.s.StopScene.Request(uid())), rpc.s.StopScene.Response).result assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Stopping assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Stopped assert ars.call_rpc( rpc.p.RemoveAction.Request(uid(), rpc.p.IdArgs(action.id)), rpc.p.RemoveAction.Response).result assert event(ars, events.p.ActionChanged).data assert ars.call_rpc( rpc.p.RemoveConstant.Request(uid(), rpc.p.RemoveConstant.Request.Args(c1.id)), rpc.p.RemoveConstant.Response).result event(ars, events.p.ProjectConstantChanged)
def call( method: Method, url: str, *, return_type: ReturnType = None, list_return_type: ReturnType = None, body: OptBody = None, params: OptParams = None, files: OptFiles = None, timeout: OptTimeout = None, ) -> ReturnValue: """Universal function for calling REST APIs. :param method: HTTP method. :param url: Resource address. :param return_type: If set, function will try to return one value of a given type. :param list_return_type: If set, function will try to return list of a given type. :param body: Data to be send in the request body. :param params: Path parameters. :param files: Instead of body, it is possible to send files. :param timeout: Specific timeout for a call. :return: Return value/type is given by return_type/list_return_type. If both are None, nothing will be returned. """ logger.debug( f"{method} {url}, body: {body}, params: {params}, files: {files is not None}, timeout: {timeout}" ) if body and files: raise RestException("Can't send data and files at the same time.") if return_type and list_return_type: raise RestException( "Only one argument from 'return_type' and 'list_return_type' can be used." ) if return_type is None: return_type = list_return_type # prepare data into dict if isinstance(body, JsonSchemaMixin): d = humps.camelize(body.to_dict()) elif isinstance(body, list): d = [] for dd in body: if isinstance(dd, JsonSchemaMixin): d.append(humps.camelize(dd.to_dict())) else: d.append(dd) elif body is not None: raise RestException("Unsupported type of data.") else: d = {} if params: params = humps.camelize(params) else: params = {} assert params is not None # requests just simply stringifies parameters, which does not work for booleans for param_name, param_value in params.items(): if isinstance(param_value, bool): params[param_name] = "true" if param_value else "false" if timeout is None: timeout = Timeout() try: if files: resp = method.value(url, files=files, timeout=timeout, params=params) else: resp = method.value(url, data=json.dumps(d), timeout=timeout, headers=headers, params=params) except requests.exceptions.RequestException as e: logger.debug("Request failed.", exc_info=True) # TODO would be good to provide more meaningful message but the original one could be very very long raise RestException("Catastrophic system error.") from e logger.debug(resp.url) # to see if query parameters are ok _handle_response(resp) if return_type is None: return None if issubclass(return_type, BytesIO): if list_return_type: raise NotImplementedError return BytesIO(resp.content) logger.debug(f"Response text: {resp.text}") try: resp_json = resp.json() except ValueError as e: logger.debug(f"Got invalid JSON in the response: {resp.text}") raise RestException("Invalid JSON.") from e logger.debug(f"Response json: {resp_json}") if isinstance(resp_json, (dict, list)): resp_json = humps.decamelize(resp_json) logger.debug(f"Decamelized json: {resp_json}") if list_return_type and not isinstance(resp_json, list): logger.debug( f"Expected list of type {return_type}, but got {resp_json}.") raise RestException("Response is not a list.") if issubclass(return_type, JsonSchemaMixin): if list_return_type: return [ dataclass_from_json(item, return_type) for item in resp_json ] else: assert not isinstance(resp_json, list) # TODO temporary workaround for bug in humps (https://github.com/nficano/humps/issues/127) from arcor2.data.object_type import Box if return_type is Box: resp_json["size_x"] = resp_json["sizex"] resp_json["size_y"] = resp_json["sizey"] resp_json["size_z"] = resp_json["sizez"] return dataclass_from_json(resp_json, return_type) else: # probably a primitive if list_return_type: return [ primitive_from_json(item, return_type) for item in resp_json ] else: assert not isinstance(resp_json, list) return primitive_from_json(resp_json, return_type)
def image_to_json(value: Image) -> str: return json.dumps(image_to_str(value))
def value_to_json(cls, value: list[Pose]) -> str: return json.dumps([v.to_json() for v in value])
def test_project_ap_rpcs(start_processes: None, ars: ARServer) -> None: upload_def(Box, BoxModel(Box.__name__, 1, 2, 3)) event(ars, events.c.ShowMainScreen) assert ars.call_rpc( rpc.s.NewScene.Request(get_id(), rpc.s.NewScene.Request.Args("Test scene")), rpc.s.NewScene.Response ).result assert len(event(ars, events.o.ChangedObjectTypes).data) == 1 scene_data = event(ars, events.s.OpenScene).data assert scene_data scene = scene_data.scene event(ars, events.s.SceneState) assert ars.call_rpc( rpc.s.AddObjectToScene.Request(get_id(), rpc.s.AddObjectToScene.Request.Args("box", Box.__name__, Pose())), rpc.s.AddObjectToScene.Response, ).result obj = event(ars, events.s.SceneObjectChanged).data assert obj # ------------------------------------------------------------------------------------------------------------------ assert ars.call_rpc( rpc.p.NewProject.Request(get_id(), rpc.p.NewProject.Request.Args(scene.id, "Project name")), rpc.p.NewProject.Response, ).result event(ars, events.s.SceneSaved) event(ars, events.p.OpenProject) event(ars, events.s.SceneState) assert ars.call_rpc( rpc.p.AddActionPoint.Request(get_id(), rpc.p.AddActionPoint.Request.Args("parent_ap", Position())), rpc.p.AddActionPoint.Response, ).result parent_ap_evt = event(ars, events.p.ActionPointChanged) assert ars.call_rpc( rpc.p.AddActionPoint.Request( get_id(), rpc.p.AddActionPoint.Request.Args("child_ap", Position(-1), parent_ap_evt.data.id) ), rpc.p.AddActionPoint.Response, ).result child_ap_evt = event(ars, events.p.ActionPointChanged) assert child_ap_evt.data.parent == parent_ap_evt.data.id lock_object(ars, child_ap_evt.data.id) assert ars.call_rpc( rpc.p.AddActionPointOrientation.Request( get_id(), rpc.p.AddActionPointOrientation.Request.Args(child_ap_evt.data.id, Orientation()) ), rpc.p.AddActionPointOrientation.Response, ).result ori = event(ars, events.p.OrientationChanged) assert ars.call_rpc( rpc.p.AddAction.Request( get_id(), rpc.p.AddAction.Request.Args( child_ap_evt.data.id, "act_name", f"{obj.id}/{Box.update_pose.__name__}", [ActionParameter("new_pose", PosePlugin.type_name(), json.dumps(ori.data.id))], [Flow()], ), ), rpc.p.AddAction.Response, ).result event(ars, events.p.ActionChanged) unlock_object(ars, child_ap_evt.data.id) ars.event_mapping[ActionChanged.__name__] = ActionChanged assert ars.call_rpc( rpc.p.CopyActionPoint.Request(get_id(), rpc.p.CopyActionPoint.Request.Args(parent_ap_evt.data.id)), rpc.p.CopyActionPoint.Response, ).result new_parent_ap = event(ars, events.p.ActionPointChanged) assert not new_parent_ap.data.parent new_child_ap = event(ars, events.p.ActionPointChanged) assert new_child_ap.data.parent == new_parent_ap.data.id new_ori = event(ars, events.p.OrientationChanged) assert new_ori.parent_id == new_child_ap.data.id # with events.p.ActionChanged it would return only BareAction (without parameters) new_action = event(ars, ActionChanged) ars.event_mapping[ActionChanged.__name__] = events.p.ActionChanged assert new_action.parent_id == new_child_ap.data.id # Pose parameter (orientation id) should be updated now assert len(new_action.data.parameters) == 1 assert json.loads(new_action.data.parameters[0].value) == new_ori.data.id