Package: mbq.apitools
mbq.apitoolsmbq.apitoolsis a python library for writing endpoints in Django. It provides a view decorator and view class that allow for strict typing of incoming query parameters and payloads, as well as consistent response shapes and status codes on the way out.Some nice things aboutmbq.apitools:All fields specified in a param or payload schema are required by default, and can be marked as optional by providing adefault=argument to the field class. The framework will automatically return a 400 response for all requests which do not conform to the specified schema. Details for each nonconforming field will be included in the response.The parsed parameters and payloads end up as rich types on the request. If a field is marked asfields.DateTime, then it will be onrequest.payloadas adatetimeobject.Pagination is handled entirely by the framework. Simply includepaginated=Truewhen you define the view, return aPaginatedResponse, and voila!All success responses have a 200 status code.All list and paginated responses contain the list of resources under the key"objects".All error responses have the same shape (an"error_code"and"detail"key).Examplefrommbq.api_toolsimportfields,responsesfrommbq.api_tools.viewsimportView,view@view("GET",permissions=[SomeDRFPermissionClass],params={"product_ids":fields.Int(default=None,many=True),"zipcode":fields.String(),})defget_categories(request):categories=Category.objects.filter(zipcode=request.params.zipcode)ifrequest.product_ids:categories=categories.filter(product_id__in=request.params.product_ids)categories=CategorySerializer(categories,many=True).datareturnresponses.ListResponse(categories)classOrdersView(View):@view.method("POST",payload={"company_id":fields.Int()})defcreate_order(self,request):order=create_order(request.payload.company_id)returnresponses.DetailResponse(OrderSerializer(order).data))@view.method("GET",permissions=[SomeDRFPermission],paginated=True)defget_orders(self,request):orders=Order.objects.all()returnresponses.PaginatedResponse(orders,OrderSerializer,request)Views@viewUse the@viewdecorator for all function based views. It accepts the following arguments:http_method_nameGET, POST, PATCH, PUT, DELETEDefaults to GETpermissionsA list of DRF permission classesorfunctions that take in a request object and returnTrueif authorized,Falseif unauthorized.paramsSchema for validating incoming query parameters. The query parameters will be available atrequest.params.payloadSchema for validating incoming payloads. The payload will be available atrequest.payload.paginatedBoolean indicating if the response will be paginated or not. Defaults toFalse.page_sizeInteger specifying the page size for a paginated response. This should only be used to override the default page size.on_unknown_field"raise"or"exclude". By default all endpoints will 400 if they receive an unknown query parameter or payload. This argument allows you to override the default behavior on a per view basis.ViewUse theViewclass when you need to support multiple HTTP verbs for the same URL pattern.
The class exposes anas_view()method to use inurls.py.All view methods on the class need to be marked with@view.method(...), which supports the exact same interface as@view. (Note that unlike Django and DRF, since the verb is specified in the decorator you can name the view methods whatever you want.)ResponsesStatus codes are predefined by the different response classes. Status codes willneverbe specified in a view.Success ResponsesAll success responses return a 200 status code.DetailResponseresponses.DetailResponse({"foo":"bar","age":10})will generate:{"foo":"bar","age":10}ListResponseListResponseaccepts any iterable of JSON serializable python objects and will nest them under an"objects"key in the response.responses.ListResponse([{"foo":"bar"}])will generate:{"objects":[{"foo":"bar"}]}PaginatedResponsePaginatedResponseaccepts aQuerySet, a serializer, and a request. It will return a properly paginated response (according to the pagination params on the request) with the data under"objects"and a"pagination"key containing the pagination information.responses.PaginatedResponse(some_queryset,SomeDRFSerializer,request)will generate:{
    "objects": [{id: 1}, {id: 2}, {id: 3}, ... ],
    "pagination": {
        "page": 1,
        "page_size": 20,
        "num_pages": 5,
        "total_objects": 89,
        "next_page": "/api/v1/orders?page=2&page_size=20",
        "previous_page": null
    }
}Alternatively, instead of a DRF Serializer class, you can pass in a function that takes in a list of objects and returns a list of serialized objects,responses.PaginatedResponse(some_queryset,lambdaobjs:[obj.to_dict()forobjinobjs],request,)See thePaginationsection for more details.Error ResponsesAll error responses will have the following shape:{"error_code":"some_unique_error_string","detail":"More details about the error..."}Some allow theerror_codeanddetailto be specified, while others have them hard-coded.ClientErrorResponse400To be used when the client made an error it could have avoided.error_codeanddetailmust be specified.responses.ClientErrorResponse("quote_state_error","Cannot approve an already approved quote")UnauthenticatedResponse401error_code"unauthenticated"detail"Authentication credentials were not provided"UnauthorizedResponse403error_code"unauthorized"detail"Unauthorized to access this resource"NotFoundResponse404error_code"not_found"detail"Resource not found"ServerValidationErrorResponse422To be used when a validation error occurs that could only be detected by the server.error_codeanddetailmust be specified.responses.ClientErrorResponse("email_already_taken","The email you have provided is already in use")TooManyRequestsResponse429error_code"too_many_requests"responses.TooManyRequestsResponse(detail="Too many requests for template")ServerErrorResponse500error_code"server_error"detail"An unexpected error occurred"ExceptionsValidationErrorUse sparingly. If raised, the framework will catch it and return generate aClientErrorResponse, with"validation_error"as theerror_codeand the error message asdetail. However, in most cases you should just catch your own errors and returnClientErrorResponse.ImmediateResponseErrorThis exception takes in a response instance and, when raised, the framework will catch it and return the response it was instantiated with. This is useful when you have some shared function between two view functions and want to quickly bail in the shared function.classSomeObjView(View):def_get_some_obj(self,id):try:returnSomeObj.objects.get(id=id)except:raiseexceptions.ImmediateResponseError(responses.NotFoundResponse())@view.method("GET")defget_some_obj(request,id=None):obj=self._get_some_obj(id)...@view.method("PATCH")defpatch_some_obj(request,id=None):obj=self._get_some_obj(id)...Schemas & FieldsSchemas and Fields use the Marshmallow library under the hood.All schema fields support the following arguments:defaultAll fields in a schema are required by default. Use thedefaultargument to both mark a field as optional and specify the default value to use if the field is not received.If you would like the field to be left out entirely of the parsed params/payload, you can dodefault=fields.OMITallow_noneDefaults toFalse. Pass inTrueif you would like to acceptNone(null) as a passed in value.validateFunction that takes in the value and returnsTrueorFalseappropriately.transformFunction that takes in the value and returns a value of the same type. This will runbeforevalidation.manyDefaults toFalse. Pass inTrueif multiple values are allowed for the field.many=Truewill result in a list of values on the parsed params/payload.For query parameters, this will support both comma separated values under a single arg (?order_ids=1,2) and multiple instances of the arg(?order_ids=1&order_ids=2).For payloads, a list of values is expected.When used withvalidateand/ortransform, these functions will be applied to each individual value.param_nameUse sparingly. It's to be used if the incoming query parameter will have a different name than the field name in the schema. For example, if the incoming query parameter will be specified as?state=foo, but you would like it to beorder_stateunderrequest.params, you would do:{"order_state": fields.String(param_name="state")}The available fields and their custom arguments are:BoolDateTimeDateTimeIntmin_valmax_valStringmin_lengthmax_lengthallow_emptyDefaults toFalse. If you would like to accept empty strings as valid values passed in, doallow_empty=True.EnumchoicesList of acceptable string valuesUUIDFloatDecimalmax_digitsmin_valmax_valEmailDictUse sparingly. Allows for any arbitrary dictionary to be passed in.Nested schemasTo nest a schema, do:@view(payload={"id":fields.Int(),"nested_object":fields.Nested({"name":fields.String()})})Nestedfields accept all of the same arguments as the other fields.What about PATCH?If you would like to support traditional PATCH behavior (where the client only sends down the fields to be updated), and need to know which fields were sent down or not, then doing something likedefault=Noneis not going to work for you. Normally, a field is either required, or has a default value and will appear on the parsed data with the default value if it was not sent down. Luckily, you can passdefault=fields.OMITto the field and it will be left out of the parsed data if it wasn't sent down. Here's an example:@view("PATCH",payload={"first_name":fields.String(default=fields.OMIT),"last_name":fields.String(default=fields.OMIT)})defupdate_person(request,id=None):person=Person.objects.get(id=id)data=request.payload.as_dict()if"first_name"indata:person.first_name=data["first_name"]if"last_name"indata:person.last_name=data["last_name"]person.save()returnresponses.DetailResponse()PaginationThe pagination mechanics are fully managed by the framework. All you have to do is specifypaginated=Truein the decorator, and return aPaginatedResponseobject.The expected query parameters whenpaginated=Truearepageandpage_size.pageobviously defaults to1, andpage_sizedefaults to20, but the client is more than welcome to send down a different value forpage_size.You can globally override the defaultpage_sizevia theAPI_TOOLS["DEFAULT_PAGE_SIZE"]setting. You can override a particular view's default page size via thepage_sizeargument to the decorator.The pagination data returned in the response under thepaginationkey looks like:{"page":1,"page_size":20,"num_pages":5,"total_objects":89,"next_page":"/api/v1/orders?page=2&page_size=20","previous_page":null}An empty page will be returned if the first page is requested and there is no data. Otherwise, if a non-existent page is requested, a 400 response will be returned.RequestsIfparamsorpayloadis specified in the decorator, then the parsed query parameters will be atrequest.paramsorrequest.payload. This will be an immutable object. If you would like it in the form of a dictionary, you can dorequest.params.as_dict().PermissionsPermissions are required. A non-empty list must be passed into the decorator. If you would like to write an unauthorized endpoint, you can do:frommbq.api_toolsimportpermissions@view(permissions=[permissions.NoAuthorization])defmy_view(request):passSettingsThe default settings are:API_TOOLS={"DEFAULT_PAGE_SIZE":20,"UNKNOWN_PAYLOAD_FIELDS":"raise","UNKNOWN_PARAM_FIELDS":"raise",}Override as you see fit.