Package: rest-helpers
rest-helpersWhat is Rest-HelpersRest-Helpers is a python 3 library that helps you build REST applications quickly, and in a consistent way.
Overall it provides the following:automated and consistent route creation via decoratorREST API versionner to handle various version your APInon verbose bindings to parse inputs from query string, body, headers ... including deserialization and validationOauth binding: adding auth to a route is as easy as adding the bindingbindingvalidatorsanddeserializersCustom autogenerated swagger UI, fully customizable via method documentation, and integrated with oktaServer side filtering via json_path and json_filter query string argumentsautomatic paging for long response payloadframework agnostic: it currently supports flask and aiohttp and is easy to extendJsonApi compliant response typeMeaningful default error messageseg: the following code snippet will create an appropriate route, document it in swagger and inject your custom modifications, take care of input binding and return meaningful responses if they are not proper, take care of exception, take care of versionning, handle server side filtering and paging, and return a proper json_api response.@swagger.swagger_objectclassPlatformResource(Resource):"""Swagger jsonapi_response:name:type: "string"example: "val_300"parent:type: "string"example: "candidate_ready""""resource_type="/worlds/platforms"def__init__(self,**kwargs):self.__dict__.update(kwargs)@routes.get_all_resources_route(platform_blueprint,PlatformResource,versionner=platform_versionner.AccretePlatformVersionner,exception_handler=exception_handler.common_exception_handler)defaccrete_platform(world_name,asof:(as_of_date.AsOfDate,from_query_string)=None,uag:from_header(header_field="User-Agent")=None,include_platform_source:(bool,from_query_string)=False,add_hoc_mixins:(list,from_query_string(as_list=True))=[]):"""Swagger doc:description: "Display the platform taking in account the inheritance tree."end swaggerSwagger parameters:User-Agent: nullend swaggerSwagger augment_default:parameters>-: ["[2]"]Arguments:world_name {str} -- The name of the world to be accreted."""returnresponses.ok(Platform("my_platform"))Automated and consistent route creation via decoratorRest-Helper follow simple routing principles:GET gets you a resource or an array of resource, and you can reuse the response payload for a PUT request in order to modify the resourceEverything else is an 'operation' and therefore uses the POST verb.Several routing decorators are providedroute decoratorEg:fromrest_helper.flask.routesimportroutes@route("/abc",options={"methods":["GET","POST"],doc=True,versionner=None,exception_handler=None)defmy_function():returnresponse.ok()This is the base decorator. It does not impose any REST related constraint and therefore should be used sparsely. It allows you to define any route.
It takes severaloptionalarguments, that are going to be similar to other routes:doc : {bool} this indicates whether or not this route should be documented in swagger.versionner : {versionnerType} this versionner is going to modify the route as defined in the versionner, more on this in the versionner section.exception_handler : {exceptionHandlerType} this is going to define how exceptions should be handled.resource based route decoratorEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.get_resource_route(HostResource,doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name,host_name):returnresponse.ok()Resource decorator takes a resource class as a first argument and create routes based on that resources. They will autogenerate
the routing scheme based on the resource_type field that must be present in every class inheriting from Resource.The route scheme is generated based on the verb and can be modified by a versionner. Eg: a get_resource route:/resource_type/resource_name/sub_resource_type/sub_resource_nameIn the example above it will lead to:/clusters/cluster_abc/hosts/host_aWith a url_root_versioner it would become:/api_version/resource_type/resource_name/sub_resource_type/sub_resource_nameThe resource type and sub resource type are extracted from the resource_type field that must be present in every class inheriting from Resource.Recomandation: resource_type should be plural.This decorator will automatically bind resource names to argument named after de-pluralized resource type:
type: clusters => cluster_name
type: hosts => host_name
type: libraries => library_nameIf an argument named resource_id is present, Rest-helpers will bind the resource_id to it.
In our example above:
resource_id => /clusters/cluster_abc/hosts/host_aA resource id uniquely identifies a resource in the entire API: there must be only one host named host_a in a cluster named "cluster_abc".
A resource name uniquely identifies a resource within the scope of their parent: there can be a host named host_a in another cluster not named "cluster_abc".This is true for all resource based routing decorators.get_resource_route decoratorEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.get_resource_route(HostResource,doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name,host_name):returnresponse.ok()This generates a route to get a single resource:GET /resource_type/resource_name/sub_resource_type/sub_resource_nameIn the example above it will lead to:GET /clusters/cluster_abc/hosts/host_aWith a url_root_versioner it would become:GET /api_version/resource_type/resource_name/sub_resource_type/sub_resource_nameget_all_resources_route decoratorEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.get_all_resources_route(HostResource,doc=True,versionner=None,page_size=10,exception_handler=None)defmy_function(cluster_name):returnresponse.ok()This generates a route to get a list of all resource under a parent (or at the root):GET /resource_type/resource_name/sub_resource_type/In the example above it will lead to:GET /clusters/cluster_abc/hosts/With a url_root_versioner it would become:GET /api_version/resource_type/resource_name/sub_resource_type/This route automatically handle the page_sizeoptionalargument (infinite by default) if used
in combination with the Rest-helpers response types. The page_size argument of the route indicate
the default value of the page size and can be overriden by a query string argumentpage_size.In addition, the resouce_id argument will be bound to the parent resource:
in the example above: resource_id => /clusters/cluster_abcput_resource_route decoratorEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.put_resource_route(HostResource,doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name,host_name):returnresponse.ok()This generates a route to PUT a resource in order to modify it:PUT /resource_type/resource_name/sub_resource_type/sub_resource_nameIn the example above it will lead to:PUT /clusters/cluster_abc/hosts/sub_resource_nameWith a url_root_versioner it would become:PUT /api_version/resource_type/resource_name/sub_resource_type/sub_resource_nameput_resource_route is intended to create or modify a resource in an idempotent way.resource_id is bound similarly to get_resource_route.patch_resource_route decoratorEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.patch_resource_route(HostResource,doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name,host_name):returnresponse.ok()This generates a route to PATCH a resource in order to modify it:PATCH /resource_type/resource_name/sub_resource_type/sub_resource_nameIn the example above it will lead to:PATCH /clusters/cluster_abc/hosts/sub_resource_nameWith a url_root_versioner it would become:PATCH /api_version/resource_type/resource_name/sub_resource_type/sub_resource_namepatch_resource_route is intended to partially update resource in an idempotent way.resource_id is bound similarly to get_resource_route.operation_resource_routeEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.operation_resource_route(HostResource,operation_name="transform",doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name,host_name):returnresponse.ok()This generates a route to PUT a resource in order to modify it:POST /resource_type/resource_name/sub_resource_type/sub_resource_name/transformIn the example above it will lead to:POST /clusters/cluster_abc/hosts/sub_resource_name/transformWith a url_root_versioner it would become:POST /api_version/resource_type/resource_name/sub_resource_type/sub_resource_name/transformOperation routes transform a resource. It can be done in a readonly way, for example a special view of the resource, or
in a permanent write way where the transformation is applied permanently. Operations can also be used for long running operation, starting a VM for instance.resource_id is bound similarly to get_resource_route.group_operation_resource_routeEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.group_operation_resource_route(HostResource,operation_name="transform",doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name):returnresponse.ok()This generates a route to PUT a resource in order to modify it:POST /resource_type/resource_name/sub_resource_type/transformIn the example above it will lead to:POST /clusters/cluster_abc/hosts/transformWith a url_root_versioner it would become:POST /api_version/resource_type/resource_name/sub_resource_type/transformGroup operations are similar to operations, except they are applied to the list of resources returned by the get_all_resources_route.
resource_id is bound similarly to get_all_resources_route.delete_resource_route decoratorEg:fromrest_helper.flask.routesimportroutesfromrest_helper.jsonapi_objectsimportResourceclassHostResource(Resource):resource_type="/clusters/hosts"@routes.put_resource_route(HostResource,doc=True,versionner=None,exception_handler=None)defmy_function(cluster_name,host_name):returnresponse.ok()This generates a route to PUT a resource in order to modify it:DELETE /resource_type/resource_name/sub_resource_type/sub_resource_nameIn the example above it will lead to:DELETE /clusters/cluster_abc/hosts/sub_resource_nameWith a url_root_versioner it would become:DELETE /api_version/resource_type/resource_name/sub_resource_type/sub_resource_nameDelete operation are to be used to delete a resource permanently.
resource_id is bound similarly to get_resource_route.VersionerThe versionner allow you to support former versions of your api. It provides several hooks to transform a request and a
response to bring it to a certain version.Eg: My service support v1 and v2, but internally, all the code supports only latest, which is v2.
A request entering via a route with a versionner will follow this path :
request at v1 -> route -> versionner: transforms a v1 request into a v2 request -> route handler|
response at v1 <- versionner: transforms a v2 response into a v1 response <- route <-|The service author defines the logic to transform a v1 request into a v2 request.The following hooks are provided:body hook: will take the current request body and return a transformed versionbody dict hook: similar to body, but act on the JSON deserialized dict bodyheaders hook: will take the request headers and return a transformed versionquery string args: will take the request query string and return a transformed versionresponse: will take the entire response object and return a transformed versionresponse body dict: will take the response body dictionary and return a transformed versionHow to define a versioner:Versioners should inherit from the BaseVersioner class.They should:define a static methosversion_routethat takes a route objet in parameter and inject the version into itdefine inner class named after supported versions and containing hooks definition.
Eg:classMyVersionner(versioning.BaseVersionner):@staticmethoddefversion_route(route):route.rule="/<rest_helper_version>"+route.ruleclassv2:passclassv1:defresponse_body_dict(self,response):v2_response["Myfield"]="hardcoded back compatible field"returnv2_responseNote that you only have to add the hooks that are useful to you.
Complete list of hook:defbody(self,body):defbody_dict(self,body):defresponse(self,response)defheaders(self,headers)defquery_string_args(self,query_string_args)defresponse_body_dict(self,response_body_dict)Url versionerThe url versionner is a commonly used versioner injecting the version at the root of the route:classUrlRootVersionner(BaseVersionner):@staticmethoddefversion_route(route):route.rule="/<rest_helper_version>"+route.ruleBy inheriting from it, you only have to define classes representing each version.Exception handlingRoutes all provide an exception_handler field that catches exceptions and respond appropriately.An exeption handler can be any callable taking exactly one parameter, the exception, and return a response.
A basic exception handler is provided and will be used by default.eg:defcommon_exception_handler(ex):ifisinstance(ex,rest_exceptions.NotFoundException):returnresponses.not_found()elifisinstance(ex,storage.PlatformNotFound):returnresponses.not_found("Platform not found.")elifisinstance(ex,exception.ExternalExceptionBase)orisinstance(ex,rest_exceptions.InvalidDataException):message="".join(ex.args).replace("\\n","\n")returnresponses.bad_request(message)elifisinstance(ex,exception.TransactionCheckFailedException):returnresponses.error(409,"Conflict","""The client expected the resource ({}) version to be{}but is actually is{}: the latest versionof the resource must be used to modify it. Get the resource again before attempting to modify it.""".format(ex.resource_id,ex.sha_was_expected_to_be,ex.actual_sha))else:returnresponses.base_exception_handler(ex)Not so verbose bindingsRest-helper bindings leverage python3 syntax to be as light and close to their target as possible.
Eg:defget_platform(platform_name,asof:(as_of_date.AsOfDate,from_query_string),uag:from_header(header_field="User-Agent")=None):In the above example, platform_name would come from the route, asof would come from the query string and is to be deserialized into anas_of_date.AsOfDateobject, uag come from the request header named "User-Agent". Both asof and uag are optional, uag is optional because it has a default value but asof is not:
not passing the asof query string argument will result into a properly formatted 400 response.Argument decorator can be either a binding or a tuple containing (type, binding). When a type is specified as the first tuple element, it will be used to
deserialize the value properly and return an appropriate 400 errors if deserialization fails. Custom types are supported:fromrest_helperimporttype_deserializerstype_deserializers.type_to_deserializer_mapping[as_of_date.AsOfDate]=bindings.as_of_deserializerEach binding detailed further can be customized via several optional arguments, but default value aim to improve readability, in all case, the foreign field (header_field for instance) will be defaulted to the variable name :asof:(as_of_date.AsOfDate,from_query_string),is equivalent toasof:(as_of_date.AsOfDate,from_query_string(query_field="asof")),You get bindings for free if you use a route decorator on top of your function. If you want to use only bindings without a route decorator,
you need to decorate your function withbind_hints.Note: all bindings are available as individual decorator as well. If that is your usage, do not forget to specify the field value.route bindingsAs mentionned in the route section some arguments are automatically bound from the url path to predictable argument_name. This is based on the resource type: for a resource typed as/worlds/platforms:a get_resource_route expect a world_name argument and a platform_name argument parsed from the uri (notice world was unpluralized)a get_all_resource_route expects only a world_name argument because it servesallplatforms under a specific world.operation_resource_route, put_resource_route, delete_resource_route, patch_resource_route will behave like get_resource_routegroup_operation_resource_route will behave like get_all_resources_routebase_binderThis is not to be used directly but can be extended to create your own binding.base_binder(field=None,validator=None,deserializer=None,type=None)fieldis name of the variable to be bound. When the value is extracted it is going to be assigned to a function argument named afterfieldvalidatoroffers a simple way to validate the data. If left toNone, and if a type is specified, a default type validator will be used. When the
validation fails, the user receives a properly formatted 400 error.
Deserializer will be used to deserialize the data.
Type is a way to infer deserialization logic from the type, more on that on the deserializer section. You cannot specify both type and deserializer.Important note: type is automatically infered from the decorator. Since validator and deserializer are generally infered from the type, you typically do not need to specify them. Field is also infered from the variable name. As a result, using the binding looks like :defmy_function(my_arg:(bool,from_query_string))This would leverage provided deserializer and validator to properly deserializing boolean from the query string. All of the following would result inmy_argbeingTrue:/myroute/?my_arg/myroute/?my_arg=True/myroute/?my_arg=TRUE/myroute/?my_arg=truefrom_json_bodyThis binding parses the request body as JSON and assign the result to the decorated argument.eg:defmy_function(data:from_json_body)field_from_json_bodyThis binding parses the request body as JSON, look for a specific field, and assign the result to the decorated argument.Use the argumentjson_fieldto specify the path to be extracted.
eg:defmy_function(data:field_from_json_body(json_field="/data/attributes/my_field"))from_headerThis binding parses the specified header and assign the result to the decorated argument.Use the argumentheader_fieldto parse from a field not named after the argument.
eg:defmy_function(uag:from_header(header_field="User-Agent")=None)from_query_stringThis binding parses the from the query string and assign the result to the decorated argument.Use the argumentquery_fieldto parse from a field not named after the argument.
Use the argumentas_list(boolean) to specify weather you want the result as a list.
eg:defmy_function(dryrun:(bool,from_query_string)=False)from_OauthThis binding parses Oauth headers and verify the token based on the open id protocol: it will decode the JWT token, contact the issuer and verify it was signed properly. It will assign the decoded token to the decorated argument.
eg:okta={"allowed_domains":config["okta_allowed_domains"],"client_id":config["okta_client_id"],"valid_tokens":config["okta_valid_tokens"],"validate_options":{"verify_at_hash":False}}defmy_function(user_auth:from_Oauth(**okta),)Note: this binding can take a deserializer that will transform the oauth value into another object.Deserialization, in detailDeserialization of inputs can be done in two ways :either specified directly in the binding, via thedeserializerargument
eg:dryrun:(bool,from_query_string(deserializer=MyDeserializer))or by linking a deserializer to a specific type: the deserializer will then be used every time this type is deserialized.
eg:fromrest_helperimporttype_deserializerstype_deserializers.type_to_deserializer_tuple_list.append((as_of_date.AsOfDate,as_of_deserializer))What can be used as a deserializerA deserializer can be any callable that takes exactly one parameter, the raw input, and returns a corresponding object.How does the type_deserializers.type_to_deserializer_tuple_list work ?This list is pretty flexible. It contains tuples where the first element should be a type, and the second element either a deserializer callable (see above), or
a tuple where the first element can generate a deserializer based on the decorator itself:eg:type_to_deserializer_tuple_list=[(bool,lambdax:xisnotNoneand(x==""orx.lower()=="true"))]dryrun:(bool,from_query_string)eg:type_to_deserializer_tuple_list=[(MyDeserializer,lambdax:xisnotNoneand(x==""orx.lower()=="true"))]dryrun:(bool,MyDeserializer())eg: we want to deserialize differently based on the decorator'safield value. If a is equal to 1 we want to deserialize the int to its value
time 2, otherwise we want to deserialize the int to its value plus 1type_to_deserializer_tuple_list=[(MyDeserializer,(lambdadecorator:lambdax:x*2ifdecorator.a==1elselambdax:x+1,))]dryrun:(int,MyDeserializer(a=1))It is a bit convoluted, but it allows for integration with various framework such as schematics.Note: type_to_deserializer_tuple_list is a list in order to take advantage of inheritance. The order matters!
Both type based deserializer and instance based deserializer will leverage inheritance, meaning that if you have a deserializer for bool,
it should bebeforethe deserializer for int since bool is a subtype of int.Validation, in detailValidation works very much alike deserialization.Validation of inputs can be done in two ways :either specified directly in the binding, via thevalidatorargument
eg:dryrun:(bool,from_query_string(validator=MyValidator))or by linking a validator to a specific type: the validator will then be used every time this type is validated.
eg:fromrest_helperimportvalidatorsvalidators.type_to_validator_tuple_list.append((as_of_date.AsOfDate,as_of_validator))How is an input validatedAn input will be validated twice : a first time before deserialization, and a second time after deserialization.What can be used as a validatorA validator can be any callable taking one argument, the object to be validated, and one optional argumentpost.postindicates whether the validator
is dealing with pre or post deserialization validation.
A validator must return a tuple where the first element is a boolean indicating whether or not validation was successful, and the second argument should be the
reason why the object is invalid, in case validation was not successful. The reason will be used to display a meaningful message to the user.How does the validators.type_to_validator_tuple_list work ?Please refer to the "How does the type_deserializers.type_to_deserializer_tuple_list work ?" section above work as the validators.type_to_validator_tuple_list
work exactly the same.Provided deserializers and validatorsBy default, the following deserializers are provided:boolintfloatDecimalstrdatetime.datetimeModel: for schematicstypes.BaseType: for schematicsBy default, the following validators are provided:Model: for schematicstypes.BaseType: for schematicsAutomated custom swagger page documentationUsing rest-helpers, you get automated documentation based on routes and bindings. If you use routes and bindings on your entry
point, they will be documented in a swagger document and try-able from a swagger UI page.Because a lot of APIs will eventually need to integrate with okta, we have added okta integration directly in the swagger UI:
people can authenticate with their username and password from the swagger ui without having to generate a token on their own.Note: you can "hide" a route by settingdoc=Falsein the route decorator arguments.What is a "custom swagger ui"? why ?There are two reasons for going with a custom swagger UI:the classic one looks very old and is not the best UXit gives us more control which is useful when adding functionalities such as okta integration, or various other UX improvements.How to enable swagger and the swagger UI ?The following code snippet will add a swagger ui at{host}/{basepath}/and a swagger document at{host}/{basepath}/swagger.jsonswagger_service_doc={"info":{"description":"This is the service description.","version":"3.0.0","title":"My Service","contact":{"email":"cicdteam@abc.com"}},"host":app.config["current_host"],"schemes":[app.config["current_scheme"]],"basePath":"/api","tags":[{"name":"my_service"}]}okta_config={"baseUrl":app.config["okta_base_url"],"clientId":app.config["okta_client_id"],"redirectUri":app.config["okta_redirect_url"]}flask.add_default_swagger_routes(app,swagger_service_doc,okta=okta_config)response typeResponse types are inferred from the route type : it is assumed that a get_resource route will return the associated resource,
and that a get_all_resource_route will return an array of associated resource (following the json_api spec). Rest-helper *does not8 (yet) automatically detect the response schema, so youmustdocument the object type that you are returning. To do so, use the@swagger.swagger_objectdecorator and document the object using yaml syntax.eg:@swagger.swagger_objectclassLibraryResource(Resource):"""Swagger jsonapi_response:name:type: "string"example: "service_name"version_pins:type: dictionaryexample: {"shared-version://my_shared_service": 1.master.1}end swagger"""You do not need to document the json_api part of the response, only the object itself.Why not automate this process ?Outside of very specific case, we believe it is next to impossible to automategooddocumentation. Good documentation
implies proper examples, explanations etc. We might however provide abasicdocumentation in the future, just like we do
with parametersHow to customize a routeThe automated swagger UI is a "best effort". There will be some case where you will want to modify what has been
automatically created. To do so, rest-helper relies on method documentation, just like we did with Resource object.5 keywords are used to achieve different goals:doc: gives you a way to update (see the python update function) the entire swagger associated swagger path dictionary. Can be used on route method documentation.parameters: gives you a way to update (see the python update function) a specific parameter dictionary, or to not document it by passing null. eg:This will ensure the user-agent parameter is not documented
Swagger parameters:
    User-Agent: nullCan be used on route method documentationextra_definition: this is to be used to add extra object definitions (not just json_api object, any kind), it is typically useful when you need subobject to defin complex objects. This can be used either in route method documentation or response object documentation.jsonapi_response: this is to be used only to document response object (see previous section)augment_default: this is the most complex and powerful one : its intent is to provide a way to augment the default dictionaryand lists. It can be applied to either route method or response object and work as follow :with regular syntax it works like the python update method but also updates nested dictionariesyou can also update a speific index in a listyou can also append to a list withyou can also delete from a list by index or by elem
To understand how augment default works, it is best to look at the corresponding tests in the test_swagger.py fileIn the documentation of a route method, use the following to customize its swagger doc:Swagger <keyword>:
<yaml>
end swaggereg:@routes.operation_resource_route(platform_blueprint,PlatformResource,operation_name="accrete",versionner=platform_versionner.AccretePlatformVersionner,exception_handler=exception_handler.common_exception_handler)defaccrete_platform(platform_name,asof:(as_of_date.AsOfDate,from_query_string)=None,uag:from_header(header_field="User-Agent")=None,include_platform_source:(bool,from_query_string)=False,add_hoc_mixins:(list,from_query_string(as_list=True))=[]):"""Swagger doc:description: "Display the platform taking in account the inheritance tree."end swaggerSwagger parameters:User-Agent: nullend swaggerSwagger augment_default:parameters>-: ["[2]"]Arguments:platform_name {str} -- The name of the platform to be accreted."""Server side filteringWhen a client is interested only in a very specific part of the response, sending back an entire response is a waste of resource: serializing it, putting it on the network and deserializing it are all significant costs that can be avoided. Specialized libraries like GraphQL do that extremly well but can be heavy to implement. Rest-helper implement a poor man's server side filtering via the json_path query string argument supported on all route methods that return a Response json_api object. While simplistic in nature, it has proven to fit most basic needs.It supports key name, list index,*(foreach) segments, and filtered foreach segments .eg:GET /resources/name
>>>
{
    data:[
        {
            "name"="name_1"
            "value"={"a":1,"b":2}
        },
        {
            "name"="name_2"
            "value"={"a":3,"b":4}
        },
    ]
}

GET /resources/name?json_path=/0/value/a
>>>
{
    data:1
}

GET /resources/name?json_path=/*/value/b
>>>
{
    data:[2,4]
}

GET /resources/name?json_path=/*:value>a~=(3|4)/value/b
>>>
{
    data:[4]
}How does array filtering workIn the last example, we used array filtering to select only some elements of the array.
When traversing an array with*you can specify a filter:*:path>to>elements>inside>the>array{operator}{value}currently the follwing operators are supported:==for equality!=for different~=for regexesThis is very basic and does not handle cases where the path does not exist on some elements.Automatic pagingSimilarly to resource filtering, paging is a common use case: returning arrays with thousands of elements is usually a waste of resource. Rest-helpers supports automatic paging for route methods returning a Response object. Paging is based on a page number and a page size. The page number comes from thepagequery string argument. The page size comes from :thepage_sizequery string argument if presentthepage_sizefield used in the Response constructor if the query string argument is not presentthepage_sizefield used in the route decorator if none of the above is present.Note: paging will not happen if page_size is not provided somehow.JSON API responsesRest-helpers offers support for JSON API compliant responses. More details about JSON API here:http://jsonapi.org. This will help to provide standard response schema making service interoperability easier.Resource base classTo take advantage of the JSON API spec implementation, your resource model object should simply inherit from theResourceclass and use the super constructor.The resource constructor accepts optional arguments to fully support the JSON API spec.classResource(object):def__init__(self,resource_type,resource_name,relationships=None,links=None,meta=None,parent=None)relationshipsshould be a dictionary of related resources, keyed by namelinksshould be a dictionaty of related links, keyed by namemetacan be any object loosely related to the resourceparentis used in the case of a nested resourceA resource is identified uniquely by the field id constructed as follows:id = /grand_parent_type_plural/grand_parent_resource_name/parent_type_plural/parent_name/type_plural/name
type = /grand_parent_type_plural/parent_type_plural/type_pluralA resource can be nested under a parent resource where it makes sense.
Eg:id = /authors/mtwain/books/tomsawyer
type = /authors/bookscall:Resource("books","tomsawyer",parent=MTwainResourceObject)Standard responseStandard responses are built on top of the JSON API resource and are defined in theresponsesmodule.responses.ok(my_resource)response.created(just_created_resource)response.bad_request(Exception("The body of the request is incorrect"))Framework agnosticRest-helper attempts to be framework agnostic: we currently support aiohttp and flask and could support any framework that support the ~100 lines adapter created for aiohttp and flask.How does it workRest helpers implement a combinaition or adapter pattern and proxy pattern. For both flask and aiohttp, we created an adapter
implementing the interface defined inframework_adapter.py. The rest of the code does not use any framework specific logic, but takes a framework adapter as the first argument of most methods. proxies are used to make this transparent to the end user: when importing theresponsemodule fromrest_helpers.flaskyou actually proxyrest_helpers.responsesbut inject a framework adapter in every call.This approach makes the pattern easily extensible, roughly a 100 lines are likely needed to onboard a new framework.Meaningful error messageOne of the philosophy behind rest-helper is to automate error messages as much as possible in order to provide meaningful error messages. As much as possible, error case lead to error message that tell the user how to correct it. We try to not respond with a blank 400 or 500 but explain in detail what failed.