def _delete_bundles(): """ Delete the bundles specified. Query parameters: - `force`: 1 to allow deletion of bundles that have descendants or that appear across multiple worksheets, or 0 to throw an error if any of the specified bundles have multiple references. Default is 0. - `recursive`: 1 to remove all bundles downstream too, or 0 otherwise. Default is 0. - `data-only`: 1 to only remove contents of the bundle(s) from the bundle store and leave the bundle metadata intact, or 0 to remove both the bundle contents and the bundle metadata. Default is 0. - `dry-run`: 1 to just return list of bundles that would be deleted with the given parameters without actually deleting them, or 0 to perform the deletion. Default is 0. """ uuids = get_resource_ids(request.json, 'bundles') force = query_get_bool('force', default=False) recursive = query_get_bool('recursive', default=False) data_only = query_get_bool('data-only', default=False) dry_run = query_get_bool('dry-run', default=False) deleted_uuids = delete_bundles(uuids, force=force, recursive=recursive, data_only=data_only, dry_run=dry_run) # Return list of deleted ids as meta return json_api_meta({}, {'ids': deleted_uuids})
def create_worksheet_items(): """ Bulk add worksheet items. |replace| - Replace existing items in host worksheets. Default is False. """ replace = query_get_bool('replace', False) new_items = WorksheetItemSchema(strict=True, many=True).load(request.json).data worksheet_to_items = {} for item in new_items: worksheet_to_items.setdefault(item['worksheet_uuid'], []).append(item) for worksheet_uuid, items in worksheet_to_items.iteritems(): worksheet_info = get_worksheet_info(worksheet_uuid, fetch_items=True) if replace: # Replace items in the worksheet update_worksheet_items( worksheet_info, [Worksheet.Item.as_tuple(i) for i in items], convert_items=False ) else: # Append items to the worksheet for item in items: add_worksheet_item(worksheet_uuid, Worksheet.Item.as_tuple(item)) return WorksheetItemSchema(many=True).dump(new_items).data
def create_worksheet_items(): """ Bulk add worksheet items. |replace| - Replace existing items in host worksheets. Default is False. """ replace = query_get_bool('replace', False) # Get the uuid of the current worksheet worksheet_uuid = query_get_type(str, 'uuid') new_items = WorksheetItemSchema(strict=True, many=True).load(request.json).data # Map of worksheet_uuid to list of items that exist in this worksheet worksheet_to_items = {} worksheet_to_items.setdefault(worksheet_uuid, []) for item in new_items: worksheet_to_items[worksheet_uuid].append(item) for worksheet_uuid, items in worksheet_to_items.items(): worksheet_info = get_worksheet_info(worksheet_uuid, fetch_items=True) if replace: # Replace items in the worksheet update_worksheet_items(worksheet_info, [Worksheet.Item.as_tuple(i) for i in items], convert_items=False) else: # Append items to the worksheet for item in items: add_worksheet_item(worksheet_uuid, Worksheet.Item.as_tuple(item)) return WorksheetItemSchema(many=True).dump(new_items).data
def create_worksheet_items(): """ Bulk add worksheet items. |replace| - Replace existing items in host worksheets. Default is False. """ replace = query_get_bool('replace', False) new_items = WorksheetItemSchema( strict=True, many=True, ).load(request.json).data worksheet_to_items = {} for item in new_items: worksheet_to_items.setdefault(item['worksheet_uuid'], []).append(item) for worksheet_uuid, items in worksheet_to_items.iteritems(): worksheet_info = get_worksheet_info(worksheet_uuid, fetch_items=True) if replace: # Replace items in the worksheet update_worksheet_items(worksheet_info, [Worksheet.Item.as_tuple(i) for i in items], convert_items=False) else: # Append items to the worksheet for item in items: add_worksheet_item(worksheet_uuid, Worksheet.Item.as_tuple(item)) return WorksheetItemSchema(many=True).dump(new_items).data
def build_bundles_document(bundle_uuids): include_set = query_get_json_api_include_set(supported={ 'owner', 'group_permissions', 'children', 'host_worksheets' }) bundles_dict = get_bundle_infos( bundle_uuids, get_children='children' in include_set, get_permissions='group_permissions' in include_set, get_host_worksheets='host_worksheets' in include_set, ignore_not_found=False, ) # Create list of bundles in original order bundles = [bundles_dict[uuid] for uuid in bundle_uuids] # Build response document document = BundleSchema(many=True).dump(bundles).data # Shim in display metadata used by the front-end application if query_get_bool('include_display_metadata', default=False): for bundle, data in zip(bundles, document['data']): bundle_class = get_bundle_subclass(bundle['bundle_type']) json_api_meta( data, { 'editable_metadata_keys': worksheet_util.get_editable_metadata_fields(bundle_class), 'metadata_type': worksheet_util.get_metadata_types(bundle_class), }, ) if 'owner' in include_set: owner_ids = set(b['owner_id'] for b in bundles if b['owner_id'] is not None) json_api_include( document, UserSchema(), local.model.get_users(user_ids=owner_ids, limit=len(owner_ids))['results'], ) if 'group_permissions' in include_set: for bundle in bundles: json_api_include(document, BundlePermissionSchema(), bundle.get('group_permissions', [])) if 'children' in include_set: for bundle in bundles: json_api_include(document, BundleSchema(), bundle.get('children', [])) if 'host_worksheets' in include_set: for bundle in bundles: json_api_include(document, WorksheetSchema(), bundle.get('host_worksheets', [])) return document
def delete_worksheets(): """ Delete the bundles specified. If |force|, allow deletion of bundles that have descendants or that appear across multiple worksheets. If |recursive|, add all bundles downstream too. If |data-only|, only remove from the bundle store, not the bundle metadata. If |dry-run|, just return list of bundles that would be deleted, but do not actually delete. """ uuids = get_resource_ids(request.json, 'worksheets') force = query_get_bool('force', default=False) for uuid in uuids: delete_worksheet(uuid, force)
def delete_worksheets(): """ Delete the bundles specified. If |force|, allow deletion of bundles that have descendants or that appear across multiple worksheets. If |recursive|, add all bundles downstream too. If |data-only|, only remove from the bundle store, not the bundle metadata. If |dry-run|, just return list of bundles that would be deleted, but do not actually delete. """ uuids = get_resource_ids(request.json, 'worksheets') force = query_get_bool('force', default=False) for uuid in uuids: delete_worksheet(uuid, force)
def _delete_bundles(): """ Delete the bundles specified. If |force|, allow deletion of bundles that have descendants or that appear across multiple worksheets. If |recursive|, add all bundles downstream too. If |data-only|, only remove from the bundle store, not the bundle metadata. If |dry-run|, just return list of bundles that would be deleted, but do not actually delete. """ uuids = get_resource_ids(request.json, 'bundles') force = query_get_bool('force', default=False) recursive = query_get_bool('recursive', default=False) data_only = query_get_bool('data-only', default=False) dry_run = query_get_bool('dry-run', default=False) deleted_uuids = delete_bundles(uuids, force=force, recursive=recursive, data_only=data_only, dry_run=dry_run) # Return list of deleted ids as meta return json_api_meta({}, {'ids': deleted_uuids})
def wrapper(*args, **kwargs): try: result = callback(*args, **kwargs) # If response is JSON, add server version to meta if isinstance(result, dict): json_api_meta(result, {'version': CODALAB_VERSION}) return result except ValidationError as err: format_errors = query_get_bool('format_errors', default=False) if format_errors: msg = err.messages else: msg = '\n'.join([e['detail'] for e in err.messages['errors']]) abort(httplib.BAD_REQUEST, msg)
def wrapper(*args, **kwargs): try: result = callback(*args, **kwargs) # If response is JSON, add server version to meta if isinstance(result, dict): json_api_meta(result, {'version': CODALAB_VERSION}) return result except ValidationError as err: format_errors = query_get_bool('format_errors', default=False) if format_errors: msg = err.messages else: msg = '\n'.join( [e['detail'] for e in err.messages['errors']]) abort(httplib.BAD_REQUEST, msg)
def _update_bundle_contents_blob(uuid): """ Update the contents of the given running or uploading bundle. Query parameters: - `urls`: (optional) comma-separated list of URLs from which to fetch data to fill the bundle, using this option will ignore any uploaded file data - `git`: (optional) 1 if URL should be interpreted as git repos to clone or 0 otherwise, default is 0. - `filename`: (optional) filename of the uploaded file, used to indicate whether or not it is an archive, default is 'contents' - `unpack`: (optional) 1 if the uploaded file should be unpacked if it is an archive, or 0 otherwise, default is 1 - `simplify`: (optional) 1 if the uploaded file should be 'simplified' if it is an archive, or 0 otherwise, default is 1. - `finalize_on_failure`: (optional) 1 if bundle state should be set to 'failed' in the case of a failure during upload, or 0 if the bundle state should not change on failure. Default is 0. - `finalize_on_success`: (optional) 1 if bundle state should be set to 'state_on_success' when the upload finishes successfully. Default is True - `state_on_success`: (optional) Update the bundle state to this state if the upload completes successfully. Must be either 'ready' or 'failed'. Default is 'ready'. """ check_bundles_have_all_permission(local.model, request.user, [uuid]) bundle = local.model.get_bundle(uuid) if bundle.state in State.FINAL_STATES: abort(http.client.FORBIDDEN, 'Contents cannot be modified, bundle already finalized.') # Get and validate query parameters finalize_on_failure = query_get_bool('finalize_on_failure', default=False) finalize_on_success = query_get_bool('finalize_on_success', default=True) final_state = request.query.get('state_on_success', default=State.READY) if finalize_on_success and final_state not in State.FINAL_STATES: abort( http.client.BAD_REQUEST, 'state_on_success must be one of %s' % '|'.join(State.FINAL_STATES), ) # If this bundle already has data, remove it. if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) # Store the data. try: sources = None if request.query.urls: sources = query_get_list('urls') # request without "filename" doesn't need to upload to bundle store if request.query.filename: filename = request.query.get('filename', default='contents') sources = [(filename, request['wsgi.input'])] if sources: local.upload_manager.upload_to_bundle_store( bundle, sources=sources, follow_symlinks=False, exclude_patterns=None, remove_sources=False, git=query_get_bool('git', default=False), unpack=query_get_bool('unpack', default=True), simplify_archives=query_get_bool('simplify', default=True), ) # See UploadManager for full explanation of 'simplify' bundle_link_url = getattr(bundle.metadata, "link_url", None) bundle_location = bundle_link_url or local.bundle_store.get_bundle_location( bundle.uuid) local.model.update_disk_metadata(bundle, bundle_location, enforce_disk_quota=True) except UsageError as err: # This is a user error (most likely disk quota overuser) so raise a client HTTP error if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) msg = "Upload failed: %s" % err local.model.update_bundle(bundle, { 'state': State.FAILED, 'metadata': { 'failure_message': msg } }) abort(http.client.BAD_REQUEST, msg) except Exception as e: # Upload failed: cleanup, update state if desired, and return HTTP error if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) msg = "Upload failed: %s" % e # The client may not want to finalize the bundle on failure, to keep # open the possibility of retrying the upload in the case of transient # failure. # Workers also use this API endpoint to upload partial contents of # running bundles, and they should use finalize_on_failure=0 to avoid # letting transient errors during upload fail the bundles prematurely. if finalize_on_failure: local.model.update_bundle(bundle, { 'state': State.FAILED, 'metadata': { 'failure_message': msg } }) abort(http.client.INTERNAL_SERVER_ERROR, msg) else: if finalize_on_success: # Upload succeeded: update state local.model.update_bundle(bundle, {'state': final_state})
def _create_bundles(): """ Bulk create bundles. Query parameters: - `worksheet`: UUID of the parent worksheet of the new bundle, add to this worksheet if not detached or shadowing another bundle. The new bundle also inherits permissions from this worksheet. - `shadow`: UUID of the bundle to "shadow" (the new bundle will be added as an item immediately after this bundle in its parent worksheet). - `detached`: 1 if should not add new bundle to any worksheet, or 0 otherwise. Default is 0. - `wait_for_upload`: 1 if the bundle state should be initialized to "uploading" regardless of the bundle type, or 0 otherwise. Used when copying bundles from another CodaLab instance, this prevents these new bundles from being executed by the BundleManager. Default is 0. """ worksheet_uuid = request.query.get('worksheet') shadow_parent_uuid = request.query.get('shadow') after_sort_key = request.query.get('after_sort_key') detached = query_get_bool('detached', default=False) if worksheet_uuid is None: abort( http.client.BAD_REQUEST, "Parent worksheet id must be specified as" "'worksheet' query parameter", ) # Deserialize bundle fields bundles = (BundleSchema(strict=True, many=True, dump_only=BUNDLE_CREATE_RESTRICTED_FIELDS).load( request.json).data) # Check for all necessary permissions worksheet = local.model.get_worksheet(worksheet_uuid, fetch_items=False) check_worksheet_has_all_permission(local.model, request.user, worksheet) worksheet_util.check_worksheet_not_frozen(worksheet) request.user.check_quota(need_time=True, need_disk=True) created_uuids = [] for bundle in bundles: # Prep bundle info for saving into database # Unfortunately cannot use the `construct` methods because they don't # provide a uniform interface for constructing bundles for all types # Hopefully this can all be unified after REST migration is complete bundle_uuid = bundle.setdefault('uuid', spec_util.generate_uuid()) created_uuids.append(bundle_uuid) bundle_class = get_bundle_subclass(bundle['bundle_type']) bundle['owner_id'] = request.user.user_id metadata = bundle.get("metadata", {}) if metadata.get("link_url"): bundle['state'] = State.READY elif issubclass(bundle_class, UploadedBundle) or query_get_bool( 'wait_for_upload', False): bundle['state'] = State.UPLOADING else: bundle['state'] = State.CREATED bundle[ 'is_anonymous'] = worksheet.is_anonymous # inherit worksheet anonymity bundle.setdefault('metadata', {})['created'] = int(time.time()) for dep in bundle.setdefault('dependencies', []): dep['child_uuid'] = bundle_uuid # Create bundle object bundle = bundle_class(bundle, strict=False) # Save bundle into model local.model.save_bundle(bundle) # Inherit worksheet permissions group_permissions = local.model.get_group_worksheet_permissions( request.user.user_id, worksheet_uuid) set_bundle_permissions([{ 'object_uuid': bundle_uuid, 'group_uuid': p['group_uuid'], 'permission': p['permission'], } for p in group_permissions]) # Add as item to worksheet if not detached: if shadow_parent_uuid is None: local.model.add_worksheet_items( worksheet_uuid, [worksheet_util.bundle_item(bundle_uuid)], after_sort_key) else: local.model.add_shadow_worksheet_items(shadow_parent_uuid, bundle_uuid) # Get created bundles bundles_dict = get_bundle_infos(created_uuids) # Return bundles in original order # Need to check if the UUID is in the dict, since there is a chance that a bundle is deleted # right after being created. bundles = [ bundles_dict[uuid] for uuid in created_uuids if uuid in bundles_dict ] return BundleSchema(many=True).dump(bundles).data
def _update_bundle_contents_blob(uuid): """ Update the contents of the given running or uploading bundle. Query parameters: - `urls`: (optional) URL from which to fetch data to fill the bundle; using this option will ignore any uploaded file data. Only supports one URL. - `git`: (optional) 1 if URL should be interpreted as git repos to clone or 0 otherwise, default is 0. - `filename`: (optional) filename of the uploaded file, used to indicate whether or not it is an archive, default is 'contents' - `unpack`: (optional) 1 if the uploaded file should be unpacked if it is an archive, or 0 otherwise, default is 1 - `finalize_on_failure`: (optional) 1 if bundle state should be set to 'failed' in the case of a failure during upload, or 0 if the bundle state should not change on failure. Default is 0. - `finalize_on_success`: (optional) 1 if bundle state should be set to 'state_on_success' when the upload finishes successfully. Default is True - `state_on_success`: (optional) Update the bundle state to this state if the upload completes successfully. Must be either 'ready' or 'failed'. Default is 'ready'. - `use_azure_blob_beta`: (optional) Use Azure Blob Storage to store the bundle. Default is False. If CODALAB_ALWAYS_USE_AZURE_BLOB_BETA is set, this parameter is disregarded, as Azure Blob Storage will always be used. """ check_bundles_have_all_permission(local.model, request.user, [uuid]) bundle = local.model.get_bundle(uuid) if bundle.state in State.FINAL_STATES: abort(http.client.FORBIDDEN, 'Contents cannot be modified, bundle already finalized.') # Get and validate query parameters finalize_on_failure = query_get_bool('finalize_on_failure', default=False) finalize_on_success = query_get_bool('finalize_on_success', default=True) use_azure_blob_beta = os.getenv("CODALAB_ALWAYS_USE_AZURE_BLOB_BETA") or query_get_bool( 'use_azure_blob_beta', default=False ) final_state = request.query.get('state_on_success', default=State.READY) if finalize_on_success and final_state not in State.FINAL_STATES: abort( http.client.BAD_REQUEST, 'state_on_success must be one of %s' % '|'.join(State.FINAL_STATES), ) # If this bundle already has data, remove it. if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) # Store the data. try: source = None if request.query.urls: sources = query_get_list('urls') if len(sources) != 1: abort(http.client.BAD_REQUEST, "Exactly one url must be provided.") source = sources[0] # request without "filename" doesn't need to upload to bundle store if request.query.filename: filename = request.query.get('filename', default='contents') source = (filename, request['wsgi.input']) bundle_link_url = getattr(bundle.metadata, "link_url", None) if bundle_link_url: # Don't upload to bundle store if using --link, as the path # already exists. pass elif source: local.upload_manager.upload_to_bundle_store( bundle, source=source, git=query_get_bool('git', default=False), unpack=query_get_bool('unpack', default=True), use_azure_blob_beta=use_azure_blob_beta, ) bundle_link_url = getattr(bundle.metadata, "link_url", None) bundle_location = bundle_link_url or local.bundle_store.get_bundle_location(bundle.uuid) local.model.update_disk_metadata(bundle, bundle_location, enforce_disk_quota=True) except UsageError as err: # This is a user error (most likely disk quota overuser) so raise a client HTTP error if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) msg = "Upload failed: %s" % err local.model.update_bundle( bundle, { 'state': State.FAILED, 'metadata': {'failure_message': msg, 'error_traceback': traceback.format_exc()}, }, ) abort(http.client.BAD_REQUEST, msg) except Exception as e: # Upload failed: cleanup, update state if desired, and return HTTP error if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) msg = "Upload failed: %s" % e # The client may not want to finalize the bundle on failure, to keep # open the possibility of retrying the upload in the case of transient # failure. # Workers also use this API endpoint to upload partial contents of # running bundles, and they should use finalize_on_failure=0 to avoid # letting transient errors during upload fail the bundles prematurely. if finalize_on_failure: local.model.update_bundle( bundle, { 'state': State.FAILED, 'metadata': {'failure_message': msg, 'error_traceback': traceback.format_exc()}, }, ) abort(http.client.INTERNAL_SERVER_ERROR, msg) else: if finalize_on_success: # Upload succeeded: update state local.model.update_bundle(bundle, {'state': final_state})
def _update_bundle_contents_blob(uuid): """ Update the contents of the given running or uploading bundle. Query parameters: urls - comma-separated list of URLs from which to fetch data to fill the bundle, using this option will ignore any uploaded file data git - (optional) 1 if URL should be interpreted as git repos to clone or 0 otherwise, default is 0 OR filename - (optional) filename of the uploaded file, used to indicate whether or not it is an archive, default is 'contents' Query parameters that are always available: unpack - (optional) 1 if the uploaded file should be unpacked if it is an archive, or 0 otherwise, default is 1 simplify - (optional) 1 if the uploaded file should be 'simplified' if it is an archive, or 0 otherwise, default is 1 (See UploadManager for full explanation of 'simplification') finalize_on_failure - (optional) True ('1') if bundle state should be set to 'failed' in the case of a failure during upload, or False ('0') if the bundle state should not change on failure. Default is False. state_on_success - (optional) Update the bundle state to this state if the upload completes successfully. Must be either 'ready' or 'failed'. Default is 'ready'. """ check_bundles_have_all_permission(local.model, request.user, [uuid]) bundle = local.model.get_bundle(uuid) if bundle.state in State.FINAL_STATES: abort(httplib.FORBIDDEN, 'Contents cannot be modified, bundle already finalized.') # Get and validate query parameters finalize_on_failure = query_get_bool('finalize_on_failure', default=False) final_state = request.query.get('state_on_success', default=State.READY) if final_state not in State.FINAL_STATES: abort( httplib.BAD_REQUEST, 'state_on_success must be one of %s' % '|'.join(State.FINAL_STATES)) # If this bundle already has data, remove it. if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) # Store the data. try: if request.query.urls: sources = query_get_list('urls') else: filename = request.query.get('filename', default='contents') sources = [(filename, request['wsgi.input'])] local.upload_manager.upload_to_bundle_store( bundle, sources=sources, follow_symlinks=False, exclude_patterns=None, remove_sources=False, git=query_get_bool('git', default=False), unpack=query_get_bool('unpack', default=True), simplify_archives=query_get_bool('simplify', default=True)) local.upload_manager.update_metadata_and_save(bundle, new_bundle=False) except Exception as e: # Upload failed: cleanup, update state if desired, and return HTTP error if local.upload_manager.has_contents(bundle): local.upload_manager.cleanup_existing_contents(bundle) msg = "Upload failed: %s" % e # The client may not want to finalize the bundle on failure, to keep # open the possibility of retrying the upload in the case of transient # failure. # Workers also use this API endpoint to upload partial contents of # running bundles, and they should use finalize_on_failure=0 to avoid # letting transient errors during upload fail the bundles prematurely. if finalize_on_failure: local.model.update_bundle(bundle, { 'state': State.FAILED, 'metadata': { 'failure_message': msg }, }) abort(httplib.INTERNAL_SERVER_ERROR, msg) else: # Upload succeeded: update state local.model.update_bundle(bundle, {'state': final_state})
def _create_bundles(): """ Bulk create bundles. |worksheet_uuid| - The parent worksheet of the bundle, add to this worksheet if not detached or shadowing another bundle. Also used to inherit permissions. |shadow| - the uuid of the bundle to shadow |detached| - True ('1') if should not add new bundle to any worksheet, or False ('0') otherwise. Default is False. |wait_for_upload| - True ('1') if the bundle state should be initialized to UPLOADING regardless of the bundle type, or False ('0') otherwise. This prevents run bundles that are being copied from another instance from being run by the BundleManager. Default is False. """ worksheet_uuid = request.query.get('worksheet') shadow_parent_uuid = request.query.get('shadow') detached = query_get_bool('detached', default=False) if worksheet_uuid is None: abort( httplib.BAD_REQUEST, "Parent worksheet id must be specified as" "'worksheet' query parameter") # Deserialize bundle fields bundles = BundleSchema( strict=True, many=True, dump_only=BUNDLE_CREATE_RESTRICTED_FIELDS, ).load(request.json).data # Check for all necessary permissions worksheet = local.model.get_worksheet(worksheet_uuid, fetch_items=False) check_worksheet_has_all_permission(local.model, request.user, worksheet) worksheet_util.check_worksheet_not_frozen(worksheet) request.user.check_quota(need_time=True, need_disk=True) created_uuids = [] for bundle in bundles: # Prep bundle info for saving into database # Unfortunately cannot use the `construct` methods because they don't # provide a uniform interface for constructing bundles for all types # Hopefully this can all be unified after REST migration is complete bundle_uuid = bundle.setdefault('uuid', spec_util.generate_uuid()) created_uuids.append(bundle_uuid) bundle_class = get_bundle_subclass(bundle['bundle_type']) bundle['owner_id'] = request.user.user_id bundle['state'] = ( State.UPLOADING if issubclass(bundle_class, UploadedBundle) or query_get_bool('wait_for_upload', False) else State.CREATED) bundle.setdefault('metadata', {})['created'] = int(time.time()) for dep in bundle.setdefault('dependencies', []): dep['child_uuid'] = bundle_uuid # Create bundle object bundle = bundle_class(bundle, strict=False) # Save bundle into model local.model.save_bundle(bundle) # Inherit worksheet permissions group_permissions = local.model.get_group_worksheet_permissions( request.user.user_id, worksheet_uuid) set_bundle_permissions([{ 'object_uuid': bundle_uuid, 'group_uuid': p['group_uuid'], 'permission': p['permission'], } for p in group_permissions]) # Add as item to worksheet if not detached: if shadow_parent_uuid is None: local.model.add_worksheet_item( worksheet_uuid, worksheet_util.bundle_item(bundle_uuid)) else: local.model.add_shadow_worksheet_items(shadow_parent_uuid, bundle_uuid) # Get created bundles bundles_dict = get_bundle_infos(created_uuids) # Return bundles in original order bundles = [bundles_dict[uuid] for uuid in created_uuids] return BundleSchema(many=True).dump(bundles).data