def delete(expected_ts): entity = self.get_entity_key(name).get() if not entity: if expected_ts: return False, { 'http_code': 412, 'text': '%s was deleted by someone else' % self.entity_kind_title.capitalize(), } else: # Unconditionally deleting it, and it's already gone -> success. return True, None if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return False, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } try: self.do_delete(entity) except EntityOperationError as exc: return False, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } return True, None
def delete(expected_ts): entity = self.get_entity_key(name).get() if not entity: if expected_ts: return False, { 'http_code': 412, 'text': '%s was deleted by someone else' % self.entity_kind_title.capitalize(), } else: # Unconditionally deleting it, and it's already gone -> success. return True, None if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return False, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } entity.record_deletion( modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_delete(entity) except EntityOperationError as exc: return False, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } model.replicate_auth_db() return True, None
def update(params, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } entity.record_revision( modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_update(entity, params) except EntityOperationError as exc: return None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return False, { 'http_code': 400, 'text': str(exc), } model.replicate_auth_db() return entity, None
def update(params, modified_by, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } try: self.do_update(entity, params, modified_by) except EntityOperationError as exc: return None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return False, { 'http_code': 400, 'text': str(exc), } return entity, None
def post(self, name): """Creates a new entity, ensuring it's indeed new (no overwrites).""" self.check_preconditions() try: body = self.parse_body() name_in_body = body.pop('name', None) if not name_in_body or name_in_body != name: raise ValueError('Missing or mismatching name in request body') if not self.is_entity_writable(name): raise ValueError('This %s is not writable' % self.entity_kind_title) entity = self.entity_kind.from_serializable_dict( serializable_dict=body, key=self.get_entity_key(name), created_ts=utils.utcnow(), created_by=api.get_current_identity()) except (TypeError, ValueError) as e: self.abort_with_error(400, text=str(e)) @ndb.transactional def create(entity): if entity.key.get(): return False, { 'http_code': 409, 'text': 'Such %s already exists' % self.entity_kind_title, } entity.record_revision( modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_create(entity) except EntityOperationError as exc: return False, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return False, { 'http_code': 400, 'text': str(exc), } model.replicate_auth_db() return True, None success, error_details = create(entity) if not success: self.abort_with_error(**error_details) self.send_response( response={'ok': True}, http_code=201, headers={ 'Last-Modified': utils.datetime_to_rfc2822(entity.modified_ts), 'Location': '%s%s' % (self.entity_url_prefix, urllib.quote(entity.key.id())), } )
def get(self, name): """Fetches entity give its name.""" self.check_preconditions() obj = self.get_entity_key(name).get() if not obj: self.abort_with_error(404, text='No such %s' % self.entity_kind_title) self.send_response( response={self.entity_kind_name: self.entity_to_dict(obj)}, headers={'Last-Modified': utils.datetime_to_rfc2822(obj.modified_ts)})
def get(self, name): """Fetches entity give its name.""" self.check_preconditions() obj = self.do_get(name, self.request) if not obj: self.abort_with_error(404, text='No such %s' % self.entity_kind_title) self.send_response( response={self.entity_kind_name: self.entity_to_dict(obj)}, headers={'Last-Modified': utils.datetime_to_rfc2822(obj.modified_ts)})
def put(self, name): """Updates an existing entity.""" try: body = self.parse_body() name_in_body = body.pop('name', None) if not name_in_body or name_in_body != name: raise ValueError('Missing or mismatching name in request body') if not self.is_entity_writable(name): raise ValueError('This %s is not writable' % self.entity_kind_title) entity_params = self.entity_kind.convert_serializable_dict(body) except (TypeError, ValueError) as e: self.abort_with_error(400, text=str(e)) @ndb.transactional def update(params, modified_by, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } try: self.do_update(entity, params, modified_by) except EntityOperationError as exc: return None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return False, { 'http_code': 400, 'text': str(exc), } return entity, None entity, error_details = update( entity_params, api.get_current_identity(), self.request.headers.get('If-Unmodified-Since')) if not entity: self.abort_with_error(**error_details) self.send_response( response={'ok': True}, http_code=200, headers={ 'Last-Modified': utils.datetime_to_rfc2822(entity.modified_ts), } )
def get(self, name): """Fetches entity give its name.""" obj = self.get_entity_key(name).get() if not obj: self.abort_with_error(404, text='No such %s' % self.entity_kind_title) self.send_response( response={ self.entity_kind_name: obj.to_serializable_dict(with_id_as='name'), }, headers={'Last-Modified': utils.datetime_to_rfc2822(obj.modified_ts)})
def post(self, name): """Creates a new entity, ensuring it's indeed new (no overwrites).""" try: body = self.parse_body() name_in_body = body.pop('name', None) if not name_in_body or name_in_body != name: raise ValueError('Missing or mismatching name in request body') if not self.is_entity_writable(name): raise ValueError('This %s is not writable' % self.entity_kind_title) entity = self.entity_kind.from_serializable_dict( serializable_dict=body, key=self.get_entity_key(name), created_by=api.get_current_identity(), modified_by=api.get_current_identity()) except (TypeError, ValueError) as e: self.abort_with_error(400, text=str(e)) @ndb.transactional def create(entity): if entity.key.get(): return False, { 'http_code': 409, 'text': 'Such %s already exists' % self.entity_kind_title, } try: self.do_create(entity) except EntityOperationError as exc: return False, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return False, { 'http_code': 400, 'text': str(exc), } return True, None success, error_details = create(entity) if not success: self.abort_with_error(**error_details) self.send_response( response={'ok': True}, http_code=201, headers={ 'Last-Modified': utils.datetime_to_rfc2822(entity.modified_ts), 'Location': '%s%s' % (self.entity_url_prefix, urllib.quote(entity.key.id())), } )
def delete(expected_ts): entity = self.get_entity_key(name).get() if not entity: if expected_ts: return None, { 'http_code': 412, 'text': '%s was deleted by someone else' % self.entity_kind_title.capitalize(), } else: # Unconditionally deleting it, and it's already gone -> success. return None, None if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } if not self.can_delete(entity): # Raising from inside a transaction produces ugly logs. Just return the # exception to be raised outside. ident = api.get_current_identity() exc = api.AuthorizationError( '"%s" has no permission to delete %s "%s"' % (ident.to_bytes(), self.entity_kind_title, name)) return exc, None entity.record_deletion(modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_delete(entity) except EntityOperationError as exc: return None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } model.replicate_auth_db() return None, None
def generate_changes(auth_db_rev): """Generates change log for entities modified in given revision. Starts a chain of tasks to process all previous revisions too (if not yet processed). """ # Already done? rev = change_log_revision_key(auth_db_rev).get() if rev: logging.info('Rev %d was already processed at %s by app ver %s', auth_db_rev, utils.datetime_to_rfc2822(rev.when), rev.app_version) return # Use kindless query to grab _all_ changed entities regardless of their kind. # Do keys only query to workaround ndb's inability to work with dynamically # generated classes (e.g. *History). q = ndb.Query(ancestor=model.historical_revision_key(auth_db_rev)) changes = [] for change_list in q.map(diff_entity_by_key, keys_only=True): changes.extend(change_list or []) logging.info('Changes found: %d', len(changes)) # Commit changes, start processing previous version if not yet done. Need to # write AuthDBLogRev marker even if there were no significant changes. @ndb.transactional def commit(): if change_log_revision_key(auth_db_rev).get(): logging.warning('Rev %d was already processed concurrently', auth_db_rev) return rev = AuthDBLogRev(key=change_log_revision_key(auth_db_rev), when=utils.utcnow(), app_version=utils.get_app_version()) ndb.put_multi(changes + [rev]) # Enqueue a task to process previous version if not yet done. if auth_db_rev > 1: prev_rev = auth_db_rev - 1 if not change_log_revision_key(prev_rev).get(): logging.info('Enqueuing task to process rev %d', prev_rev) enqueue_process_change_task(prev_rev) commit()
def delete(expected_ts): entity = self.get_entity_key(name).get() if not entity: if expected_ts: return None, { 'http_code': 412, 'text': '%s was deleted by someone else' % self.entity_kind_title.capitalize(), } else: # Unconditionally deleting it, and it's already gone -> success. return None, None if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } if not self.can_delete(entity): # Raising from inside a transaction produces ugly logs. Just return the # exception to be raised outside. ident = api.get_current_identity() exc = api.AuthorizationError( '"%s" has no permission to delete %s "%s"' % (ident.to_bytes(), self.entity_kind_title, name)) return exc, None entity.record_deletion( modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_delete(entity) except EntityOperationError as exc: return None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } model.replicate_auth_db() return None, None
def update(params, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } if not self.can_update(entity): # Raising from inside a transaction produces ugly logs. Just return the # exception to be raised outside. ident = api.get_current_identity() exc = api.AuthorizationError( '"%s" has no permission to update %s "%s"' % (ident.to_bytes(), self.entity_kind_title, name)) return None, exc, None entity.record_revision( modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_update(entity, params) except EntityOperationError as exc: return None, None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return None, None, { 'http_code': 400, 'text': str(exc), } model.replicate_auth_db() return entity, None, None
def generate_changes(auth_db_rev): """Generates change log for entities modified in given revision. Starts a chain of tasks to process all previous revisions too (if not yet processed). """ # Already done? rev = change_log_revision_key(auth_db_rev).get() if rev: logging.info( 'Rev %d was already processed at %s by app ver %s', auth_db_rev, utils.datetime_to_rfc2822(rev.when), rev.app_version) return # Use kindless query to grab _all_ changed entities regardless of their kind. # Do keys only query to workaround ndb's inability to work with dynamically # generated classes (e.g. *History). q = ndb.Query(ancestor=model.historical_revision_key(auth_db_rev)) changes = [] for change_list in q.map(diff_entity_by_key, keys_only=True): changes.extend(change_list or []) logging.info('Changes found: %d', len(changes)) # Commit changes, start processing previous version if not yet done. Need to # write AuthDBLogRev marker even if there were no significant changes. @ndb.transactional def commit(): if change_log_revision_key(auth_db_rev).get(): logging.warning('Rev %d was already processed concurrently', auth_db_rev) return rev = AuthDBLogRev( key=change_log_revision_key(auth_db_rev), when=utils.utcnow(), app_version=utils.get_app_version()) ndb.put_multi(changes + [rev]) # Enqueue a task to process previous version if not yet done. if auth_db_rev > 1: prev_rev = auth_db_rev - 1 if not change_log_revision_key(prev_rev).get(): logging.info('Enqueuing task to process rev %d', prev_rev) enqueue_process_change_task(prev_rev) commit()
def update(params, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } if not self.can_update(entity): # Raising from inside a transaction produces ugly logs. Just return the # exception to be raised outside. ident = api.get_current_identity() exc = api.AuthorizationError( '"%s" has no permission to update %s "%s"' % (ident.to_bytes(), self.entity_kind_title, name)) return None, exc, None entity.record_revision(modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_update(entity, params) except EntityOperationError as exc: return None, None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return None, None, { 'http_code': 400, 'text': str(exc), } model.replicate_auth_db() return entity, None, None
def test_datetime_to_rfc2822(self): self.assertEqual( 'Mon, 02 Jan 2012 03:04:05 -0000', utils.datetime_to_rfc2822(datetime.datetime(2012, 1, 2, 3, 4, 5)))
def put(self, name): """Updates an existing entity.""" self.check_preconditions() try: body = self.parse_body() name_in_body = body.pop('name', None) if not name_in_body or name_in_body != name: raise ValueError('Missing or mismatching name in request body') if not self.is_entity_writable(name): raise ValueError('This %s is not writable' % self.entity_kind_title) entity_params = self.entity_kind.convert_serializable_dict(body) except (TypeError, ValueError) as e: self.abort_with_error(400, text=str(e)) @ndb.transactional def update(params, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } if not self.can_update(entity): # Raising from inside a transaction produces ugly logs. Just return the # exception to be raised outside. ident = api.get_current_identity() exc = api.AuthorizationError( '"%s" has no permission to update %s "%s"' % (ident.to_bytes(), self.entity_kind_title, name)) return None, exc, None entity.record_revision( modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_update(entity, params) except EntityOperationError as exc: return None, None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return None, None, { 'http_code': 400, 'text': str(exc), } model.replicate_auth_db() return entity, None, None entity, exc, error_details = update( entity_params, self.request.headers.get('If-Unmodified-Since')) if exc: raise exc # pylint: disable=raising-bad-type if not entity: self.abort_with_error(**error_details) self.send_response( response={'ok': True}, http_code=200, headers={ 'Last-Modified': utils.datetime_to_rfc2822(entity.modified_ts), } )
def put(self, name): """Updates an existing entity.""" self.check_preconditions() try: body = self.parse_body() name_in_body = body.pop('name', None) if not name_in_body or name_in_body != name: raise ValueError('Missing or mismatching name in request body') if not self.is_entity_writable(name): raise ValueError('This %s is not writable' % self.entity_kind_title) entity_params = self.entity_kind.convert_serializable_dict(body) except (TypeError, ValueError) as e: self.abort_with_error(400, text=str(e)) @ndb.transactional def update(params, expected_ts): entity = self.get_entity_key(name).get() if not entity: return None, None, { 'http_code': 404, 'text': 'No such %s' % self.entity_kind_title, } if (expected_ts and utils.datetime_to_rfc2822(entity.modified_ts) != expected_ts): return None, None, { 'http_code': 412, 'text': '%s was modified by someone else' % self.entity_kind_title.capitalize(), } if not self.can_update(entity): # Raising from inside a transaction produces ugly logs. Just return the # exception to be raised outside. ident = api.get_current_identity() exc = api.AuthorizationError( '"%s" has no permission to update %s "%s"' % (ident.to_bytes(), self.entity_kind_title, name)) return None, exc, None entity.record_revision(modified_by=api.get_current_identity(), modified_ts=utils.utcnow(), comment='REST API') try: self.do_update(entity, params) except EntityOperationError as exc: return None, None, { 'http_code': 409, 'text': exc.message, 'details': exc.details, } except ValueError as exc: return None, None, { 'http_code': 400, 'text': str(exc), } model.replicate_auth_db() return entity, None, None entity, exc, error_details = update( entity_params, self.request.headers.get('If-Unmodified-Since')) if exc: raise exc # pylint: disable=raising-bad-type if not entity: self.abort_with_error(**error_details) self.send_response(response={'ok': True}, http_code=200, headers={ 'Last-Modified': utils.datetime_to_rfc2822(entity.modified_ts), })