async def head(self): """ Handles HEAD request. 1. Validates GET parameters using GET input schema and validator. 2. Fetches total amount of items and returns it in X-Total header. 3. Finishes response. :raises: In case of bad query parameters - HTTPError 400. """ await self.validate( { k: self.get_argument(k) for k in self.request.query_arguments.keys() }, self.get_schema_input) try: qs = self.get_queryset(paginate=False) except AttributeError as e: # Wrong field name in filter or order_by # Request.body is not available in HEAD request # No detail info will be provided raise HTTPError(400) try: total_num = await self.application.objects.count(qs) except (peewee.DataError, peewee.ProgrammingError, ValueError) as e: # Bad parameters # Request.body is not available in HEAD request # No detail info will be provided raise HTTPError(400) self.set_header('X-Total', total_num) self.finish()
async def post(self): """ Handles POST request. Validates data and creates new item. Returns serialized object written to response. HTTPError 405 is raised in case of not creatable model (there must be _create method implemented in model class). HTTPError 400 is raised in case of violated constraints, invalid parameters and other data and integrity errors. :raises: HTTPError 405, 400 """ data = await self.validate(self.request.body, self.post_schema_input) try: item = await self.model_cls._create(self.application, data) except AttributeError as e: # We can only create item if _create() model method implemented err = xhtml_escape(str(e)) raise HTTPError(405, body=self.get_response(errors=[{ 'code': '', 'message': 'Method not allowed', 'detail': err }])) except (peewee.IntegrityError, peewee.DataError) as e: raise HTTPError(400, body=self.get_response(errors=[{ 'code': '', 'message': 'Invalid parameters', 'detail': str(e) }])) self.response(result=await self.serialize(item))
async def delete(self, item_id): """ Handles DELETE request. _delete method must be defined to handle delete logic. If method is not defined, HTTP 405 is raised. If deletion is finished, writes to response HTTP code 200 and a message 'Item deleted'. :raises: HTTPError 405 if model object is not deletable. """ # DELETE usually does not have body to validate. await self.validate(self.request.body or {}, self.delete_schema_input, item_id=item_id) item = await self.get_item(item_id) try: # We can only delete item if model method _delete() is implemented await item._delete(self.application) except AttributeError as e: raise HTTPError(405, body=self.get_response(errors=[{ 'code': '', 'message': 'Method not allowed', 'detail': str(e) }])) self.response(result='Item deleted')
async def get(self): """ Handles GET request. 1. Validates GET parameters using GET input schema and validator. 2. Executes query using given query parameters. 3. Paginates. 4. Serializes result. 5. Writes to response, not finishing it. :raises: In case of bad query parameters - HTTP 400. """ await self.validate( { k: self.get_argument(k) for k in self.request.query_arguments.keys() }, self.get_schema_input) try: qs = self.get_queryset() except AttributeError as e: # Wrong field name in filter or order_by raise HTTPError( 400, body=self.get_response(errors=[{ 'code': '', 'message': 'Bad query arguments', 'detail': xhtml_escape(str(e)) }])) items, pagination = await self._get_items(qs) result = [] for m in items: result.append(await self.serialize(m)) self.response(result={'items': result}, pagination=pagination)
async def bad_permissions(self): """ Returns answer of access denied. :raises: HTTPError 401 """ raise HTTPError( 401, body=self.get_response(errors=[{ 'code': '', 'message': 'Access denied' }]))
async def put(self, item_id): """ Handles PUT request. Validates data and updates given item. Returns serialized model. Raises 405 in case of not updatable model (there must be _update method implemented in model class). Raises 400 in case of violated constraints, invalid parameters and other data and integrity errors. :raises: HTTP 405, HTTP 400. """ item = await self.get_item(item_id) data = await self.validate(self.request.body, self.put_schema_input, item_id=item_id) try: item = await item._update(self.application, data) except AttributeError as e: # We can only update item if model method _update is implemented raise HTTPError( 405, body=self.get_response(errors=[{ 'code': '', 'message': 'Method not allowed', 'detail': xhtml_escape(str(e)) }])) except (peewee.IntegrityError, peewee.DataError) as e: raise HTTPError(400, body=self.get_response(errors=[{ 'code': '', 'message': 'Invalid parameters', 'detail': str(e) }])) self.response(result=await self.serialize(item))
async def validate(self, data, schema, **kwargs): """ Method to validate parameters. Raises HTTPError(400) with error info for invalid data. :param data: bytes or dict :param schema: dict, valid JSON schema (http://json-schema.org/latest/json-schema-validation.html) :return: None if data is not valid. Else dict(data) """ # Get and parse arguments if isinstance(data, dict): _data = data # pragma: no cover else: try: _data = json.loads(data.decode()) except ValueError as exc: # json.loads error raise HTTPError( 400, body=self.get_response(errors=[{ 'code': '', 'message': 'Request body is not a valid json object', 'detail': str(exc) }])) v = validator_for(schema)(schema) errors = [] for error in v.iter_errors(_data): # error is an instance of jsonschema.exceptions.ValidationError err_msg = xhtml_escape(error.message) errors.append({ 'code': '', 'message': 'Validation failed', 'detail': err_msg }) if errors: # data does not pass validation raise HTTPError(400, body=self.get_response(errors=errors)) return _data
async def _get_items(self, qs): """ Gets queryset and paginates it. It executes database query. If total amount of items should be received (self.total = True), queries are executed in parallel. :param qs: peewee queryset :return: tuple: executed query, pagination info (dict) :raises: In case of bad query parameters - HTTP 400. """ pagination = {'offset': self.offset} try: if self.total: # Execute requests to database in parallel (items + total) awaitables = [] qs_total = self.get_queryset(paginate=False) if self.prefetch_queries: # Support of prefetch queries awaitables.append( self.application.objects.prefetch( qs, *self.prefetch_queries)) else: awaitables.append(self.application.objects.execute(qs)) awaitables.append(self.application.objects.count(qs_total)) items, total = await multi(awaitables) # Set total items number pagination['total'] = total else: if self.prefetch_queries: items = await self.application.objects.prefetch( qs, *self.prefetch_queries) else: items = await self.application.objects.execute(qs) except (peewee.DataError, ValueError) as e: # Bad parameters raise HTTPError(400, body=self.get_response(errors=[{ 'code': '', 'message': 'Bad query arguments', 'detail': str(e) }])) # Set number of fetched items pagination['limit'] = len(items) # TODO WTF? Why limit is set? return items, pagination
async def get_item(self, item_id): """ Fetches item from database by PK. Result is cached in self._instance for multiple calls :raises: HTTP 404 if no item found. :returns: raw object if exists. :rtype: ORM model instance. """ if not self._instance: try: self._instance = await self.application.objects.get( self.get_queryset(item_id)) except (self.model_cls.DoesNotExist, ValueError) as e: raise HTTPError(404, body=self.get_response(errors=[{ 'code': '', 'message': 'Item not found', 'detail': str(e) }])) return self._instance