{
"info": {
"author": "Guillaume Grosbois",
"author_email": "grosbois.guillaume@gmail.com",
"bugtrack_url": null,
"classifiers": [
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.5"
],
"description": "[](https://travis-ci.com/WillFr/rest-helpers)\n\n[](https://coveralls.io/github/WillFr/rest-helpers?branch=master)\n\n# rest-helpers\n\n## What is Rest-Helpers\nRest-Helpers is a python 3 library that helps you build REST applications quickly, and in a consistent way.\nOverall it provides the following:\n- [automated and consistent route creation via decorator](#route-decorator-section)\n- [REST API versionner to handle various version your API](#versioner-section)\n- [non verbose bindings to parse inputs from query string, body, headers ... including deserialization and validation](#binding-section)\n- [Oauth binding: adding auth to a route is as easy as adding the binding](#oauth-binding-section)\n- binding [validators](#validation-section) and [deserializers](#deserialization-section)\n- [Custom autogenerated swagger UI, fully customizable via method documentation, and integrated with okta](#swagger-section)\n- [Server side filtering via json_path and json_filter query string arguments](#serverside-filtering)\n- [automatic paging for long response payload](#automatic-paging-section)\n- [framework agnostic: it currently supports flask and aiohttp and is easy to extend](#framework-agnostic-section)\n- [JsonApi compliant response type](#json-api-section)\n- [Meaningful default error messages](#error-messages-section)\n\neg: 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.\n```python\n@swagger.swagger_object\nclass PlatformResource(Resource):\n \"\"\"\n Swagger jsonapi_response:\n name:\n type: \"string\"\n example: \"val_300\"\n parent:\n type: \"string\"\n example: \"candidate_ready\"\n \"\"\"\n\n resource_type = \"/worlds/platforms\"\n\n def __init__(self, **kwargs):\n self.__dict__.update(kwargs)\n\n@routes.get_all_resources_route(platform_blueprint, PlatformResource, versionner=platform_versionner.AccretePlatformVersionner, exception_handler=exception_handler.common_exception_handler)\ndef accrete_platform(\n world_name,\n asof:(as_of_date.AsOfDate,from_query_string)=None,\n uag:from_header(header_field=\"User-Agent\")=None,\n include_platform_source:(bool, from_query_string)=False,\n add_hoc_mixins:(list, from_query_string(as_list=True))= []\n ):\n \"\"\"\n Swagger doc:\n description: \"Display the platform taking in account the inheritance tree.\"\n end swagger\n\n Swagger parameters:\n User-Agent: null\n end swagger\n\n Swagger augment_default:\n parameters>-: [\"[2]\"]\n\n Arguments:\n world_name {str} -- The name of the world to be accreted.\n \"\"\"\n return responses.ok(Platform(\"my_platform\"))\n\n```\n\n\n## Automated and consistent route creation via decorator\nRest-Helper follow simple routing principles:\n- 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 resource\n- Everything else is an 'operation' and therefore uses the POST verb.\n\nSeveral routing decorators are provided\n\n### route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\n\n\n@route(\"/abc\", options={\"methods\": [\"GET\", \"POST\"], doc=True, versionner=None, exception_handler=None)\ndef my_function():\n return response.ok()\n```\n\nThis 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.\nIt takes several *optional* arguments, that are going to be similar to other routes:\n- doc : {bool} this indicates whether or not this route should be documented in swagger.\n- versionner : {versionnerType} this versionner is going to modify the route as defined in the versionner, more on this in the versionner section.\n- exception_handler : {exceptionHandlerType} this is going to define how exceptions should be handled.\n\n### resource based route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.get_resource_route(HostResource, doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name, host_name):\n return response.ok()\n```\n\nResource decorator takes a resource class as a first argument and create routes based on that resources. They will autogenerate\nthe routing scheme based on the resource_type field that must be present in every class inheriting from Resource.\n\nThe route scheme is generated based on the verb and can be modified by a versionner. Eg: a get_resource route:\n`/resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nIn the example above it will lead to:\n`/clusters/cluster_abc/hosts/host_a`\n\nWith a url_root_versioner it would become:\n`/api_version/resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nThe resource type and sub resource type are extracted from the resource_type field that must be present in every class inheriting from Resource.\n\nRecomandation: resource_type should be plural.\n\nThis decorator will automatically bind resource names to argument named after de-pluralized resource type:\ntype: clusters => cluster_name\ntype: hosts => host_name\ntype: libraries => library_name\n\nIf an argument named resource_id is present, Rest-helpers will bind the resource_id to it.\nIn our example above:\nresource_id => /clusters/cluster_abc/hosts/host_a\n\nA 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\".\nA 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\".\n\nThis is true for all resource based routing decorators.\n\n### get_resource_route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.get_resource_route(HostResource, doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name, host_name):\n return response.ok()\n```\nThis generates a route to get a single resource:\n`GET /resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nIn the example above it will lead to:\n`GET /clusters/cluster_abc/hosts/host_a`\n\nWith a url_root_versioner it would become:\n`GET /api_version/resource_type/resource_name/sub_resource_type/sub_resource_name`\n\n### get_all_resources_route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.get_all_resources_route(HostResource, doc=True, versionner=None, page_size=10, exception_handler=None)\ndef my_function(cluster_name):\n return response.ok()\n```\nThis generates a route to get a list of all resource under a parent (or at the root):\n`GET /resource_type/resource_name/sub_resource_type/`\n\nIn the example above it will lead to:\n`GET /clusters/cluster_abc/hosts/`\n\nWith a url_root_versioner it would become:\n`GET /api_version/resource_type/resource_name/sub_resource_type/`\n\nThis route automatically handle the page_size *optional* argument (infinite by default) if used\nin combination with the Rest-helpers response types. The page_size argument of the route indicate\nthe default value of the page size and can be overriden by a query string argument `page_size`.\n\nIn addition, the resouce_id argument will be bound to the parent resource:\nin the example above: resource_id => /clusters/cluster_abc\n\n### put_resource_route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.put_resource_route(HostResource, doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name, host_name):\n return response.ok()\n```\nThis generates a route to PUT a resource in order to modify it:\n`PUT /resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nIn the example above it will lead to:\n`PUT /clusters/cluster_abc/hosts/sub_resource_name`\n\nWith a url_root_versioner it would become:\n`PUT /api_version/resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nput_resource_route is intended to create or modify a resource in an idempotent way.\n\nresource_id is bound similarly to get_resource_route.\n\n\n### patch_resource_route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.patch_resource_route(HostResource, doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name, host_name):\n return response.ok()\n```\nThis generates a route to PATCH a resource in order to modify it:\n`PATCH /resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nIn the example above it will lead to:\n`PATCH /clusters/cluster_abc/hosts/sub_resource_name`\n\nWith a url_root_versioner it would become:\n`PATCH /api_version/resource_type/resource_name/sub_resource_type/sub_resource_name`\n\npatch_resource_route is intended to partially update resource in an idempotent way.\n\nresource_id is bound similarly to get_resource_route.\n\n\n\n### operation_resource_route\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.operation_resource_route(HostResource, operation_name=\"transform\", doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name, host_name):\n return response.ok()\n```\nThis generates a route to PUT a resource in order to modify it:\n`POST /resource_type/resource_name/sub_resource_type/sub_resource_name/transform`\n\nIn the example above it will lead to:\n`POST /clusters/cluster_abc/hosts/sub_resource_name/transform`\n\nWith a url_root_versioner it would become:\n`POST /api_version/resource_type/resource_name/sub_resource_type/sub_resource_name/transform`\n\nOperation routes transform a resource. It can be done in a readonly way, for example a special view of the resource, or\nin a permanent write way where the transformation is applied permanently. Operations can also be used for long running operation, starting a VM for instance.\n\nresource_id is bound similarly to get_resource_route.\n\n### group_operation_resource_route\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.group_operation_resource_route(HostResource, operation_name=\"transform\", doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name):\n return response.ok()\n```\nThis generates a route to PUT a resource in order to modify it:\n`POST /resource_type/resource_name/sub_resource_type/transform`\n\nIn the example above it will lead to:\n`POST /clusters/cluster_abc/hosts/transform`\n\nWith a url_root_versioner it would become:\n`POST /api_version/resource_type/resource_name/sub_resource_type/transform`\n\nGroup operations are similar to operations, except they are applied to the list of resources returned by the get_all_resources_route.\nresource_id is bound similarly to get_all_resources_route.\n\n### delete_resource_route decorator\nEg:\n```python\nfrom rest_helper.flask.routes import routes\nfrom rest_helper.jsonapi_objects import Resource\n\nclass HostResource(Resource):\n resource_type = \"/clusters/hosts\"\n\n@routes.put_resource_route(HostResource, doc=True, versionner=None, exception_handler=None)\ndef my_function(cluster_name, host_name):\n return response.ok()\n```\nThis generates a route to PUT a resource in order to modify it:\n`DELETE /resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nIn the example above it will lead to:\n`DELETE /clusters/cluster_abc/hosts/sub_resource_name`\n\nWith a url_root_versioner it would become:\n`DELETE /api_version/resource_type/resource_name/sub_resource_type/sub_resource_name`\n\nDelete operation are to be used to delete a resource permanently.\nresource_id is bound similarly to get_resource_route.\n\n\n\n## Versioner\n\nThe versionner allow you to support former versions of your api. It provides several hooks to transform a request and a\nresponse to bring it to a certain version.\n\nEg: My service support v1 and v2, but internally, all the code supports only latest, which is v2.\nA request entering via a route with a versionner will follow this path :\nrequest at v1 -> route -> versionner: transforms a v1 request into a v2 request -> route handler|\nresponse at v1 <- versionner: transforms a v2 response into a v1 response <- route <-|\n\nThe service author defines the logic to transform a v1 request into a v2 request.\n\nThe following hooks are provided:\n- body hook: will take the current request body and return a transformed version\n- body dict hook: similar to body, but act on the JSON deserialized dict body\n- headers hook: will take the request headers and return a transformed version\n- query string args: will take the request query string and return a transformed version\n- response: will take the entire response object and return a transformed version\n- response body dict: will take the response body dictionary and return a transformed version\n\n### How to define a versioner:\nVersioners should inherit from the BaseVersioner class.\n\nThey should:\n- define a static methos `version_route` that takes a route objet in parameter and inject the version into it\n- define inner class named after supported versions and containing hooks definition.\nEg:\n```python\nclass MyVersionner(versioning.BaseVersionner):\n @staticmethod\n def version_route(route):\n route.rule=\"/\"+route.rule\n\n class v2:\n pass\n\n class v1:\n def response_body_dict(self, response):\n v2_response[\"Myfield\"] = \"hardcoded back compatible field\"\n\n return v2_response\n```\n\nNote that you only have to add the hooks that are useful to you.\nComplete list of hook:\n```python\ndef body(self, body):\ndef body_dict(self, body):\ndef response(self, response)\ndef headers(self, headers)\ndef query_string_args(self, query_string_args)\ndef response_body_dict(self, response_body_dict)\n```\n\n### Url versioner\nThe url versionner is a commonly used versioner injecting the version at the root of the route:\n```python\nclass UrlRootVersionner(BaseVersionner):\n @staticmethod\n def version_route(route):\n route.rule=\"/\"+route.rule\n```\n\nBy inheriting from it, you only have to define classes representing each version.\n\n\n\n\n## Exception handling\nRoutes all provide an exception_handler field that catches exceptions and respond appropriately.\n\nAn exeption handler can be any callable taking exactly one parameter, the exception, and return a response.\nA basic exception handler is provided and will be used by default.\n\neg:\n```python\ndef common_exception_handler(ex):\n if isinstance(ex, rest_exceptions.NotFoundException):\n return responses.not_found()\n elif isinstance(ex, storage.PlatformNotFound):\n return responses.not_found(\"Platform not found.\")\n elif isinstance(ex, exception.ExternalExceptionBase) or isinstance(ex, rest_exceptions.InvalidDataException):\n message = \"\".join(ex.args).replace(\"\\\\n\", \"\\n\")\n return responses.bad_request(message)\n elif isinstance(ex, exception.TransactionCheckFailedException):\n return responses.error(409, \"Conflict\", \"\"\"\n The client expected the resource ({}) version to be {} but is actually is {}: the latest version\n of 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))\n else:\n return responses.base_exception_handler(ex)\n```\n## Not so verbose bindings\n\nRest-helper bindings leverage python3 syntax to be as light and close to their target as possible.\nEg:\n```python\ndef get_platform(\n platform_name,\n asof:(as_of_date.AsOfDate,from_query_string),\n uag:from_header(header_field=\"User-Agent\")=None\n ):\n```\n\nIn the above example, platform_name would come from the route, asof would come from the query string and is to be deserialized into an `as_of_date.AsOfDate`\nobject, 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:\nnot passing the asof query string argument will result into a properly formatted 400 response.\n\nArgument 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\ndeserialize the value properly and return an appropriate 400 errors if deserialization fails. Custom types are supported:\n\n```python\nfrom rest_helper import type_deserializers\ntype_deserializers.type_to_deserializer_mapping[as_of_date.AsOfDate] = bindings.as_of_deserializer\n```\n\nEach 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 :\n```python\nasof:(as_of_date.AsOfDate,from_query_string),\n```\nis equivalent to\n```python\nasof:(as_of_date.AsOfDate,from_query_string(query_field=\"asof\")),\n```\n\nYou 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,\nyou need to decorate your function with `bind_hints`.\n\nNote: all bindings are available as individual decorator as well. If that is your usage, do not forget to specify the field value.\n\n### route bindings\nAs 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`:\n- a get_resource_route expect a world_name argument and a platform_name argument parsed from the uri (notice world was unpluralized)\n- a get_all_resource_route expects only a world_name argument because it serves *all* platforms under a specific world.\n- operation_resource_route, put_resource_route, delete_resource_route, patch_resource_route will behave like get_resource_route\n- group_operation_resource_route will behave like get_all_resources_route\n\n### base_binder\nThis is not to be used directly but can be extended to create your own binding.\n\n```python\nbase_binder(field=None, validator=None, deserializer=None, type=None)\n```\n\n`field` is name of the variable to be bound. When the value is extracted it is going to be assigned to a function argument named after `field`\n`validator` offers a simple way to validate the data. If left to `None`, and if a type is specified, a default type validator will be used. When the\nvalidation fails, the user receives a properly formatted 400 error.\nDeserializer will be used to deserialize the data.\nType is a way to infer deserialization logic from the type, more on that on the deserializer section. You cannot specify both type and deserializer.\n\nImportant 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 :\n```python\ndef my_function(my_arg: (bool, from_query_string))\n```\nThis would leverage provided deserializer and validator to properly deserializing boolean from the query string. All of the following would result in `my_arg` being `True`:\n- /myroute/?my_arg\n- /myroute/?my_arg=True\n- /myroute/?my_arg=TRUE\n- /myroute/?my_arg=true\n\n### from_json_body\nThis binding parses the request body as JSON and assign the result to the decorated argument.\n\neg:\n```python\ndef my_function(\n data:from_json_body\n)\n```\n\n### field_from_json_body\nThis binding parses the request body as JSON, look for a specific field, and assign the result to the decorated argument.\n\nUse the argument `json_field` to specify the path to be extracted.\neg:\n```python\ndef my_function(\n data:field_from_json_body(json_field=\"/data/attributes/my_field\")\n)\n```\n\n### from_header\nThis binding parses the specified header and assign the result to the decorated argument.\n\nUse the argument `header_field` to parse from a field not named after the argument.\neg:\n```python\ndef my_function(\n uag:from_header(header_field=\"User-Agent\")=None\n)\n```\n\n### from_query_string\nThis binding parses the from the query string and assign the result to the decorated argument.\n\nUse the argument `query_field` to parse from a field not named after the argument.\nUse the argument `as_list` (boolean) to specify weather you want the result as a list.\neg:\n```python\ndef my_function(\n dryrun:(bool,from_query_string)=False\n)\n```\n\n\n### from_Oauth\nThis 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.\neg:\n```python\nokta = {\n \"allowed_domains\":config[\"okta_allowed_domains\"],\n \"client_id\":config[\"okta_client_id\"],\n \"valid_tokens\": config[\"okta_valid_tokens\"],\n \"validate_options\": {\"verify_at_hash\": False}\n}\n\ndef my_function(\n user_auth: from_Oauth(**okta),\n)\n```\n\nNote: this binding can take a deserializer that will transform the oauth value into another object.\n\n\n\n## Deserialization, in detail\nDeserialization of inputs can be done in two ways :\n- either specified directly in the binding, via the `deserializer` argument\n eg: `dryrun:(bool,from_query_string(deserializer=MyDeserializer))`\n- or by linking a deserializer to a specific type: the deserializer will then be used every time this type is deserialized.\n eg:\n```python\nfrom rest_helper import type_deserializers\ntype_deserializers.type_to_deserializer_tuple_list.append((as_of_date.AsOfDate, as_of_deserializer))\n```\n\n### What can be used as a deserializer\nA deserializer can be any callable that takes exactly one parameter, the raw input, and returns a corresponding object.\n\n### How does the type_deserializers.type_to_deserializer_tuple_list work ?\nThis 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\na tuple where the first element can generate a deserializer based on the decorator itself:\n\neg:\n```python\ntype_to_deserializer_tuple_list = [(bool, lambda x: x is not None and (x == \"\" or x.lower() == \"true\" ))]\ndryrun:(bool,from_query_string)\n```\n\neg:\n```python\ntype_to_deserializer_tuple_list = [(MyDeserializer, lambda x: x is not None and (x == \"\" or x.lower() == \"true\" ))]\ndryrun:(bool,MyDeserializer())\n```\n\neg: we want to deserialize differently based on the decorator's `a` field value. If a is equal to 1 we want to deserialize the int to its value\ntime 2, otherwise we want to deserialize the int to its value plus 1\n```python\ntype_to_deserializer_tuple_list = [(MyDeserializer, (lambda decorator: lambda x:x*2 if decorator.a == 1 else lambda x:x+1,))]\ndryrun:(int,MyDeserializer(a=1))\n```\n\nIt is a bit convoluted, but it allows for integration with various framework such as schematics.\n\nNote: type_to_deserializer_tuple_list is a list in order to take advantage of inheritance. The order matters!\nBoth type based deserializer and instance based deserializer will leverage inheritance, meaning that if you have a deserializer for bool,\nit should be *before* the deserializer for int since bool is a subtype of int.\n\n\n\n## Validation, in detail\nValidation works very much alike deserialization.\n\nValidation of inputs can be done in two ways :\n- either specified directly in the binding, via the `validator` argument\n eg: `dryrun:(bool,from_query_string(validator=MyValidator))`\n- or by linking a validator to a specific type: the validator will then be used every time this type is validated.\n eg:\n```python\nfrom rest_helper import validators\nvalidators.type_to_validator_tuple_list.append((as_of_date.AsOfDate, as_of_validator))\n```\n\n### How is an input validated\nAn input will be validated twice : a first time before deserialization, and a second time after deserialization.\n\n### What can be used as a validator\nA validator can be any callable taking one argument, the object to be validated, and one optional argument `post`. `post` indicates whether the validator\nis dealing with pre or post deserialization validation.\nA 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\nreason why the object is invalid, in case validation was not successful. The reason will be used to display a meaningful message to the user.\n\n### How does the validators.type_to_validator_tuple_list work ?\nPlease 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\nwork exactly the same.\n\n### Provided deserializers and validators\nBy default, the following deserializers are provided:\n- bool\n- int\n- float\n- Decimal\n- str\n- datetime.datetime\n- Model: for schematics\n- types.BaseType: for schematics\n\nBy default, the following validators are provided:\n- Model: for schematics\n- types.BaseType: for schematics\n\n\n\n## Automated custom swagger page documentation\n\nUsing rest-helpers, you get automated documentation based on routes and bindings. If you use routes and bindings on your entry\npoint, they will be documented in a swagger document and try-able from a swagger UI page.\n\nBecause a lot of APIs will eventually need to integrate with okta, we have added okta integration directly in the swagger UI:\npeople can authenticate with their username and password from the swagger ui without having to generate a token on their own.\n\nNote: you can \"hide\" a route by setting `doc=False` in the route decorator arguments.\n\n### What is a \"custom swagger ui\"? why ?\nThere are two reasons for going with a custom swagger UI:\n- the classic one looks very old and is not the best UX\n- it gives us more control which is useful when adding functionalities such as okta integration, or various other UX improvements.\n\n### How to enable swagger and the swagger UI ?\n\nThe following code snippet will add a swagger ui at `{host}/{basepath}/` and a swagger document at `{host}/{basepath}/swagger.json`\n\n```python\nswagger_service_doc = {\n \"info\":{\n \"description\": \"This is the service description.\",\n \"version\": \"3.0.0\",\n \"title\": \"My Service\",\n \"contact\":{\n \"email\": \"cicdteam@abc.com\"\n }\n },\n \"host\": app.config[\"current_host\"],\n \"schemes\": [app.config[\"current_scheme\"]],\n \"basePath\": \"/api\",\n \"tags\":[{\"name\": \"my_service\"}]\n}\nokta_config= {\n \"baseUrl\":app.config[\"okta_base_url\"],\n \"clientId\": app.config[\"okta_client_id\"],\n \"redirectUri\": app.config[\"okta_redirect_url\"]\n}\nflask.add_default_swagger_routes(app, swagger_service_doc, okta=okta_config)\n```\n### response type\nResponse types are inferred from the route type : it is assumed that a get_resource route will return the associated resource,\nand 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 you *must* document the object type that you are returning. To do so, use the `@swagger.swagger_object` decorator and document the object using yaml syntax.\n\neg:\n```python\n@swagger.swagger_object\nclass LibraryResource(Resource):\n \"\"\"\n Swagger jsonapi_response:\n name:\n type: \"string\"\n example: \"service_name\"\n version_pins:\n type: dictionary\n example: {\"shared-version://my_shared_service\": 1.master.1}\n end swagger\n \"\"\"\n```\n\nYou do not need to document the json_api part of the response, only the object itself.\n\n*Why not automate this process ?*\nOutside of very specific case, we believe it is next to impossible to automate *good* documentation. Good documentation\nimplies proper examples, explanations etc. We might however provide a *basic* documentation in the future, just like we do\nwith parameters\n\n\n### How to customize a route\n\nThe automated swagger UI is a \"best effort\". There will be some case where you will want to modify what has been\nautomatically created. To do so, rest-helper relies on method documentation, just like we did with Resource object.\n\n5 keywords are used to achieve different goals:\n- 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.\n- 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:\n```\nThis will ensure the user-agent parameter is not documented\nSwagger parameters:\n User-Agent: null\n```\nCan be used on route method documentation\n- extra_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.\n- jsonapi_response: this is to be used only to document response object (see previous section)\n- augment_default: this is the most complex and powerful one : its intent is to provide a way to augment the default dictionary *and lists*. It can be applied to either route method or response object and work as follow :\n- - with regular syntax it works like the python update method but also updates nested dictionaries\n- - you can also update a speific index in a list\n- - you can also append to a list with\n- - you can also delete from a list by index or by elem\nTo understand how augment default works, it is best to look at the corresponding tests in the test_swagger.py file\n\nIn the documentation of a route method, use the following to customize its swagger doc:\n```\nSwagger :\n\nend swagger\n```\n\neg:\n```python\n@routes.operation_resource_route(platform_blueprint, PlatformResource, operation_name=\"accrete\",versionner=platform_versionner.AccretePlatformVersionner, exception_handler=exception_handler.common_exception_handler)\ndef accrete_platform(\n platform_name,\n asof:(as_of_date.AsOfDate,from_query_string)=None,\n uag:from_header(header_field=\"User-Agent\")=None,\n include_platform_source:(bool, from_query_string)=False,\n add_hoc_mixins:(list, from_query_string(as_list=True))= []\n ):\n \"\"\"\n Swagger doc:\n description: \"Display the platform taking in account the inheritance tree.\"\n end swagger\n\n Swagger parameters:\n User-Agent: null\n end swagger\n\n Swagger augment_default:\n parameters>-: [\"[2]\"]\n\n Arguments:\n platform_name {str} -- The name of the platform to be accreted.\n \"\"\"\n\n```\n\n\n\n## Server side filtering\n\nWhen 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.\n\nIt supports key name, list index, `*` (foreach) segments, and filtered foreach segments .\n\neg:\n```\nGET /resources/name\n>>>\n{\n data:[\n {\n \"name\"=\"name_1\"\n \"value\"={\"a\":1,\"b\":2}\n },\n {\n \"name\"=\"name_2\"\n \"value\"={\"a\":3,\"b\":4}\n },\n ]\n}\n\nGET /resources/name?json_path=/0/value/a\n>>>\n{\n data:1\n}\n\nGET /resources/name?json_path=/*/value/b\n>>>\n{\n data:[2,4]\n}\n\nGET /resources/name?json_path=/*:value>a~=(3|4)/value/b\n>>>\n{\n data:[4]\n}\n```\n\n### How does array filtering work\nIn the last example, we used array filtering to select only some elements of the array.\nWhen traversing an array with `*` you can specify a filter:\n`*:path>to>elements>inside>the>array{operator}{value}`\n\ncurrently the follwing operators are supported:\n- `==` for equality\n- `!=` for different\n- `~=` for regexes\n\nThis is very basic and does not handle cases where the path does not exist on some elements.\n\n\n\n## Automatic paging\n\nSimilarly 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 the `page` query string argument. The page size comes from :\n1. the `page_size` query string argument if present\n2. the `page_size` field used in the Response constructor if the query string argument is not present\n3. the `page_size` field used in the route decorator if none of the above is present.\n\nNote: paging will not happen if page_size is not provided somehow.\n\n\n\n## JSON API responses\n\nRest-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.\n\n### Resource base class\nTo take advantage of the JSON API spec implementation, your resource model object should simply inherit from the `Resource` class and use the super constructor.\n\nThe resource constructor accepts optional arguments to fully support the JSON API spec.\n```python\nclass Resource(object):\n def __init__(self, resource_type, resource_name, relationships=None, links=None, meta=None, parent=None)\n```\n\n- `relationships` should be a dictionary of related resources, keyed by name\n- `links` should be a dictionaty of related links, keyed by name\n- `meta` can be any object loosely related to the resource\n- `parent` is used in the case of a nested resource\n\nA resource is identified uniquely by the field id constructed as follows:\n```\nid = /grand_parent_type_plural/grand_parent_resource_name/parent_type_plural/parent_name/type_plural/name\ntype = /grand_parent_type_plural/parent_type_plural/type_plural\n```\nA resource can be nested under a parent resource where it makes sense.\nEg:\n```\nid = /authors/mtwain/books/tomsawyer\ntype = /authors/books\n```\ncall: `Resource(\"books\",\"tomsawyer\",parent=MTwainResourceObject)`\n\n### Standard response\nStandard responses are built on top of the JSON API resource and are defined in the `responses` module.\n```python\nresponses.ok(my_resource)\nresponse.created(just_created_resource)\nresponse.bad_request(Exception(\"The body of the request is incorrect\"))\n```\n\n\n\n## Framework agnostic\n\nRest-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.\n\n### How does it work\nRest helpers implement a combinaition or adapter pattern and proxy pattern. For both flask and aiohttp, we created an adapter\nimplementing the interface defined in `framework_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 the `response` module from `rest_helpers.flask` you actually proxy `rest_helpers.responses` but inject a framework adapter in every call.\n\nThis approach makes the pattern easily extensible, roughly a 100 lines are likely needed to onboard a new framework.\n\n\n\n## Meaningful error message\n\nOne 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.\n\n",
"description_content_type": "text/markdown",
"docs_url": null,
"download_url": "",
"downloads": {
"last_day": -1,
"last_month": -1,
"last_week": -1
},
"home_page": "https://github.com/WillFr/rest-helpers",
"keywords": "",
"license": "",
"maintainer": "",
"maintainer_email": "",
"name": "rest-helpers",
"package_url": "https://pypi.org/project/rest-helpers/",
"platform": "",
"project_url": "https://pypi.org/project/rest-helpers/",
"project_urls": {
"Homepage": "https://github.com/WillFr/rest-helpers"
},
"release_url": "https://pypi.org/project/rest-helpers/1.16/",
"requires_dist": [
"setuptools (>=0.5)",
"flask",
"pyyaml",
"requests",
"schematics",
"python-dateutil",
"requests-futures",
"httpretty",
"aiohttp (>=2.3.0)",
"aiotask-context",
"pytest-aiohttp",
"pytest-asyncio",
"cryptography",
"python-jose[cryptography]",
"jinja2"
],
"requires_python": "",
"summary": "A set of method to help creating rest services",
"version": "1.16"
},
"last_serial": 5519848,
"releases": {
"1.11": [
{
"comment_text": "",
"digests": {
"md5": "10f579fee61991dcbcc9c6b6e841cf51",
"sha256": "9a1f6e87582e3d1aad1c8181231f03f581c8dffc67fc679b35a303aefcc54b08"
},
"downloads": -1,
"filename": "rest_helpers-1.11-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "10f579fee61991dcbcc9c6b6e841cf51",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61212,
"upload_time": "2019-05-03T23:32:00",
"url": "https://files.pythonhosted.org/packages/79/3f/3530c166f84d20c892425136339d3da7238e57621e6026bf5b6650976416/rest_helpers-1.11-py2.py3-none-any.whl"
}
],
"1.12.1": [
{
"comment_text": "",
"digests": {
"md5": "36ca55f9f4a142a5d15671d9b4cca345",
"sha256": "dbaedf1a60846c49e9b2a2de4d3d554ebdf30c62fc46f0a4c7c2ae8455ffe587"
},
"downloads": -1,
"filename": "rest_helpers-1.12.1-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "36ca55f9f4a142a5d15671d9b4cca345",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61665,
"upload_time": "2019-05-15T19:54:42",
"url": "https://files.pythonhosted.org/packages/4c/00/4e3b5b5b2961d08ad773915f1cfe06b6c58eec4c26028af24d20a45bc919/rest_helpers-1.12.1-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "6cb307d21b870415e1603e893e6b9cd8",
"sha256": "3724eef6c8370845482367f2b60fdcd85a5e53bc1443ad483ccea72d6d55c5e6"
},
"downloads": -1,
"filename": "rest-helpers-1.12.1.tar.gz",
"has_sig": false,
"md5_digest": "6cb307d21b870415e1603e893e6b9cd8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 68280,
"upload_time": "2019-05-15T19:54:44",
"url": "https://files.pythonhosted.org/packages/25/2b/fe9d769ba9f1d02c28c39386e88abe37a1bfb1ae6fb75c874bc21b441eab/rest-helpers-1.12.1.tar.gz"
}
],
"1.13": [
{
"comment_text": "",
"digests": {
"md5": "e2c0eaf907b0d8a4df34f929636982e6",
"sha256": "77b529a7b2322b46ce0f7ca35af28fc4ade5a534bc24d2483f3c141b78bf95c5"
},
"downloads": -1,
"filename": "rest_helpers-1.13-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "e2c0eaf907b0d8a4df34f929636982e6",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61731,
"upload_time": "2019-07-09T20:01:13",
"url": "https://files.pythonhosted.org/packages/f7/f6/3e3a8d38c694533a88630babe78ed8877b52af1b7928f54a0f34fe4e2d94/rest_helpers-1.13-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "bd94a66bdab94f1e1d495fd9a26000cc",
"sha256": "9ceb3cab78da02ace87d551b20b0d0cbdb5c62288a8c5a4add4980bb595a4d96"
},
"downloads": -1,
"filename": "rest-helpers-1.13.tar.gz",
"has_sig": false,
"md5_digest": "bd94a66bdab94f1e1d495fd9a26000cc",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 68346,
"upload_time": "2019-07-09T20:01:15",
"url": "https://files.pythonhosted.org/packages/00/86/bf3272748ca0f6d813d12aa6e70d92c78db35dd62895cd380f4182e163c7/rest-helpers-1.13.tar.gz"
}
],
"1.14": [
{
"comment_text": "",
"digests": {
"md5": "77263745ab12ffc9d24c47f69d0c2c2b",
"sha256": "1e827fed5d1ecb29283dd4c55cb0c4237b63d7a0636ceb11e7e43e77dce69d63"
},
"downloads": -1,
"filename": "rest_helpers-1.14-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "77263745ab12ffc9d24c47f69d0c2c2b",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61741,
"upload_time": "2019-07-09T23:28:09",
"url": "https://files.pythonhosted.org/packages/6f/46/d85be8471f23441dd330035bf933e3a149a33e60ec57ebc6230db975cbbd/rest_helpers-1.14-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "f84215452be9c5dc2592b7bf92a1e57f",
"sha256": "60baf7d7a95bd01e5bb1f93a48af18ec0ab9b3dfaf8fd89014eeb8288b6d5a3e"
},
"downloads": -1,
"filename": "rest-helpers-1.14.tar.gz",
"has_sig": false,
"md5_digest": "f84215452be9c5dc2592b7bf92a1e57f",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 68367,
"upload_time": "2019-07-09T23:28:12",
"url": "https://files.pythonhosted.org/packages/b9/27/e922d53b3995e37938779139b855eacdf22de84de9c927ac9151f269a4ac/rest-helpers-1.14.tar.gz"
}
],
"1.15": [
{
"comment_text": "",
"digests": {
"md5": "85dcf13c200ff2d30b8790417922f6e1",
"sha256": "5a076d60e3d349f7f8b1a8f7e350b545e778603eff2ea9b8fefa52d994d3e6f4"
},
"downloads": -1,
"filename": "rest_helpers-1.15-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "85dcf13c200ff2d30b8790417922f6e1",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61744,
"upload_time": "2019-07-10T00:23:55",
"url": "https://files.pythonhosted.org/packages/a3/a5/069e430b70f5de525c9e88afd3e626b43de27ebfe841a5b8faad812cdf93/rest_helpers-1.15-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "db7052721121efe43cb4df44bd22929e",
"sha256": "5183f2e1ec168282b2a141a894b99dc042559e83b61e63132e81e8117c9f2803"
},
"downloads": -1,
"filename": "rest-helpers-1.15.tar.gz",
"has_sig": false,
"md5_digest": "db7052721121efe43cb4df44bd22929e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 68367,
"upload_time": "2019-07-10T00:23:57",
"url": "https://files.pythonhosted.org/packages/46/41/e8c4bfdbf318528ca685a5251e8c4acb2a5e394226fb74781b6004f4d7a7/rest-helpers-1.15.tar.gz"
}
],
"1.16": [
{
"comment_text": "",
"digests": {
"md5": "695a2853dd007a331d45f084040a35c4",
"sha256": "e7f0489ce64c0d28ad5e26e0571d964c3c3b9ae51339da5bc2fc4fc17f02b663"
},
"downloads": -1,
"filename": "rest_helpers-1.16-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "695a2853dd007a331d45f084040a35c4",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61857,
"upload_time": "2019-07-11T20:06:04",
"url": "https://files.pythonhosted.org/packages/15/e3/fb7188b29f8e2489c5400d1a67a5a2f5e67eca073e145d36446f6d52b27d/rest_helpers-1.16-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "99c1d3a12dd50ae888224104ecce45d2",
"sha256": "acfd3adc5cc860373519bf409d05b8ae9a594f3bf67aab3a71f0348af23f0c53"
},
"downloads": -1,
"filename": "rest-helpers-1.16.tar.gz",
"has_sig": false,
"md5_digest": "99c1d3a12dd50ae888224104ecce45d2",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 68428,
"upload_time": "2019-07-11T20:06:06",
"url": "https://files.pythonhosted.org/packages/83/98/53460e030a5cf55855878c86d95bb618fe1cfb35788a17cbfd3461a9ca30/rest-helpers-1.16.tar.gz"
}
],
"1.2": [
{
"comment_text": "",
"digests": {
"md5": "410c3cbafde5f3e182374697ad35d0a6",
"sha256": "2da500e3899ab99dc7e65f3df39ccc1d83591037285c4d27d7e75d9764aae144"
},
"downloads": -1,
"filename": "rest_helpers-1.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "410c3cbafde5f3e182374697ad35d0a6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 62154,
"upload_time": "2018-11-17T00:50:24",
"url": "https://files.pythonhosted.org/packages/0a/8a/c2287af32fc4b6586780a7ab4e51a68a23c0c1487255e3ae19f1ce234636/rest_helpers-1.2-py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "6580928be0b5b0cf87c156bc2bf18162",
"sha256": "f55ae2561af0ef2e850662e3db7270961e70b1f140070b7b2bd35a39225ebaec"
},
"downloads": -1,
"filename": "rest-helpers-1.2.tar.gz",
"has_sig": false,
"md5_digest": "6580928be0b5b0cf87c156bc2bf18162",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 61081,
"upload_time": "2018-11-17T00:50:25",
"url": "https://files.pythonhosted.org/packages/a6/c5/63ccc6e441cd4b2bcea6ea853de81a0bed2459762c84ba7f2d76ff991d97/rest-helpers-1.2.tar.gz"
}
],
"1.3": [
{
"comment_text": "",
"digests": {
"md5": "0196d5bc27cecaad6e664c8b8df91eaa",
"sha256": "6166f0d4a72591e9a6a64906d58a53fc431d2eee162aad758c7d8d8f22f40a54"
},
"downloads": -1,
"filename": "rest_helpers-1.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "0196d5bc27cecaad6e664c8b8df91eaa",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 66726,
"upload_time": "2018-11-20T18:26:14",
"url": "https://files.pythonhosted.org/packages/a1/4f/c1c49532c60fbf03fa7ea92cfffb50fc687a0d032d567aceff2a578639e4/rest_helpers-1.3-py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "6a6d342f5f87bbf5afa10e535726042b",
"sha256": "a4ded3c927f662af1d872623c0a369d369637c6505527c1493b9084f506b4368"
},
"downloads": -1,
"filename": "rest-helpers-1.3.tar.gz",
"has_sig": false,
"md5_digest": "6a6d342f5f87bbf5afa10e535726042b",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 65494,
"upload_time": "2018-11-20T18:26:16",
"url": "https://files.pythonhosted.org/packages/87/0b/2bd95b63802007d0fdc63c331ae16adb9daeabe4e6c3e35aa49dfb2f0058/rest-helpers-1.3.tar.gz"
}
],
"1.3.1": [
{
"comment_text": "",
"digests": {
"md5": "e37e5a1eeefe62ce93a48886201feb6a",
"sha256": "d6d30019fb08ce21dd0361cea063c4db326a4d70ec1cd999a69ad269cafeb26d"
},
"downloads": -1,
"filename": "rest_helpers-1.3.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e37e5a1eeefe62ce93a48886201feb6a",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 67028,
"upload_time": "2018-11-20T23:39:33",
"url": "https://files.pythonhosted.org/packages/0c/3c/0358a1f532419d3c1ba60c81d4731a9e2f53e6c2515b962d4430b1b0e430/rest_helpers-1.3.1-py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "45f53d1c9cf27bb6b456ac59bb3eb6f4",
"sha256": "83b647c12605e0b203f673c492427964e473220e5ca9e59c8cf5876c7cf414da"
},
"downloads": -1,
"filename": "rest-helpers-1.3.1.tar.gz",
"has_sig": false,
"md5_digest": "45f53d1c9cf27bb6b456ac59bb3eb6f4",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 65914,
"upload_time": "2018-11-20T23:39:34",
"url": "https://files.pythonhosted.org/packages/ea/7f/27ad8bb1821b99823977be5d7363a569be2d166e22b9b682d56273089ac5/rest-helpers-1.3.1.tar.gz"
}
],
"1.4": [
{
"comment_text": "",
"digests": {
"md5": "7e22dbb956e5edb922320dd5bb005a5a",
"sha256": "432b719964a4ef928d533604338b6119fef106c1b2a4765d59780adce789d816"
},
"downloads": -1,
"filename": "rest_helpers-1.4-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "7e22dbb956e5edb922320dd5bb005a5a",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 60630,
"upload_time": "2018-12-06T07:03:13",
"url": "https://files.pythonhosted.org/packages/91/a2/080230dcc9d0cb5526616e381aab336f86d48169150e09a0a665bb141870/rest_helpers-1.4-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "8f3a25b75c2e9c1f4530698ada969d69",
"sha256": "3374872ff7ff86411271ac4415b9ffb0738b6712fc00a647979246e5e62731fa"
},
"downloads": -1,
"filename": "rest-helpers-1.4.tar.gz",
"has_sig": false,
"md5_digest": "8f3a25b75c2e9c1f4530698ada969d69",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 66624,
"upload_time": "2018-12-06T07:03:15",
"url": "https://files.pythonhosted.org/packages/d1/f0/6f46916d909e8a567e50f3a6c0edd8f7a902a7929da3662d32f357015452/rest-helpers-1.4.tar.gz"
}
],
"1.5": [
{
"comment_text": "",
"digests": {
"md5": "631a839f3c187dc018c678325ec45871",
"sha256": "a0bd83452180944203391c7f1b3f070c6e5289dddd58e442f40e3fb4f377ae63"
},
"downloads": -1,
"filename": "rest_helpers-1.5-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "631a839f3c187dc018c678325ec45871",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 60912,
"upload_time": "2018-12-11T00:11:00",
"url": "https://files.pythonhosted.org/packages/ac/89/77d377cfac8c7dedfdbc3f4ed349333e875ba6ba763ff170314599189957/rest_helpers-1.5-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "02c0f3b7f55c53d28d41e0e8961e358b",
"sha256": "688314ef83a86b45272155b925bb8202823dc1918f8781265e9a8b94c0e262ec"
},
"downloads": -1,
"filename": "rest-helpers-1.5.tar.gz",
"has_sig": false,
"md5_digest": "02c0f3b7f55c53d28d41e0e8961e358b",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 66923,
"upload_time": "2018-12-11T00:11:02",
"url": "https://files.pythonhosted.org/packages/95/f9/b9a68b40d0ff011b7eddc2e4a5f37b42f75ffd5304f6998dd09f8498493d/rest-helpers-1.5.tar.gz"
}
],
"1.5.1": [
{
"comment_text": "",
"digests": {
"md5": "3455c78c71f7149e736539dbbafb6044",
"sha256": "080512582fc094e48ee347a66cbb03e338036b058986c1c043b034e154f3cb6e"
},
"downloads": -1,
"filename": "rest_helpers-1.5.1-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "3455c78c71f7149e736539dbbafb6044",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 60957,
"upload_time": "2018-12-12T00:05:59",
"url": "https://files.pythonhosted.org/packages/b3/d8/6ed67b9ee5993a4677573341ab5245d0e4e1777652f27aa8fa25a47f8cdc/rest_helpers-1.5.1-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "eede0b3c573f8e294f6478a0c22bac5f",
"sha256": "e2ce09e03d4452cec05bb028ed284fdca462255e054c8f32b3d645f9295c03b5"
},
"downloads": -1,
"filename": "rest-helpers-1.5.1.tar.gz",
"has_sig": false,
"md5_digest": "eede0b3c573f8e294f6478a0c22bac5f",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 66983,
"upload_time": "2018-12-12T00:06:00",
"url": "https://files.pythonhosted.org/packages/e7/26/649f68b29e1c081e291436e1c2d1f6283b44a7e13fb29f2a209e2b3d7b72/rest-helpers-1.5.1.tar.gz"
}
],
"1.6": [
{
"comment_text": "",
"digests": {
"md5": "25982eb447699bcb44139896369e8890",
"sha256": "e62e22bf069ec70210cd8a83103068b2836f0e0558fd12678c0dfcf3aca412b5"
},
"downloads": -1,
"filename": "rest_helpers-1.6-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "25982eb447699bcb44139896369e8890",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 60951,
"upload_time": "2018-12-13T23:16:22",
"url": "https://files.pythonhosted.org/packages/38/ef/32fcb31c537aec7475674523e24877386c67fd0a3e5b2cf786cc4cad163a/rest_helpers-1.6-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "65c658d05e71e0d56cb3d29413b55e7d",
"sha256": "330ef6872a8b3039dcb83cdc464d206bf8c5ef7065fe54f928a24a1cdc6d95b2"
},
"downloads": -1,
"filename": "rest-helpers-1.6.tar.gz",
"has_sig": false,
"md5_digest": "65c658d05e71e0d56cb3d29413b55e7d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 66983,
"upload_time": "2018-12-13T23:16:23",
"url": "https://files.pythonhosted.org/packages/45/aa/4b6357ffb996239c173e83fef7bd27ccf494947a6a1914e7e20df647fdce/rest-helpers-1.6.tar.gz"
}
],
"1.7": [
{
"comment_text": "",
"digests": {
"md5": "eca980dbbcbae121141df0cec7c50930",
"sha256": "1ad93dedcc9bcdef84b5a68a9841b2b18b855c99b15f971bd6f881755e38368d"
},
"downloads": -1,
"filename": "rest_helpers-1.7-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "eca980dbbcbae121141df0cec7c50930",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61018,
"upload_time": "2018-12-20T00:01:39",
"url": "https://files.pythonhosted.org/packages/9b/5a/751f954a08bd58885f34c852a246b785c6f300e8b1c652f924ab93fcc55e/rest_helpers-1.7-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "c4b1e58dd5acfdde5234d6f1430475c9",
"sha256": "b6e35ef25a8d34f3f945017f3c3c75b5d82a2eec540b497802c425122761dde7"
},
"downloads": -1,
"filename": "rest-helpers-1.7.tar.gz",
"has_sig": false,
"md5_digest": "c4b1e58dd5acfdde5234d6f1430475c9",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 67061,
"upload_time": "2018-12-20T00:01:41",
"url": "https://files.pythonhosted.org/packages/64/47/1653505aff7c40f26f397977a03ccca68cd9fe847139c1b63f27b6338e28/rest-helpers-1.7.tar.gz"
}
],
"1.9": [
{
"comment_text": "",
"digests": {
"md5": "c83897b059e63ac87b5c30d5f0e92efc",
"sha256": "bcd9e60e6bd95ec3822ee3ffde3f6f8d420f4827957b8ad1678988430670c1eb"
},
"downloads": -1,
"filename": "rest_helpers-1.9-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "c83897b059e63ac87b5c30d5f0e92efc",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61027,
"upload_time": "2019-01-30T18:28:32",
"url": "https://files.pythonhosted.org/packages/74/6f/d1c894614f28eb2633fc4633eba5b72f86d15787a5803b762a3c29cb787e/rest_helpers-1.9-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "d65bde90a0dc27f1adb284d7755739dc",
"sha256": "1dd653a5f89df5b36a356791a0cf837c87415c3ff7a9eea7b368d8bb93aeb91f"
},
"downloads": -1,
"filename": "rest-helpers-1.9.tar.gz",
"has_sig": false,
"md5_digest": "d65bde90a0dc27f1adb284d7755739dc",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 67077,
"upload_time": "2019-01-30T18:28:33",
"url": "https://files.pythonhosted.org/packages/7a/89/b824bc153fc5b56848ccff06d175d4c2c33c0e32fa099de8fa01ca3cf852/rest-helpers-1.9.tar.gz"
}
],
"1.9.1": [
{
"comment_text": "",
"digests": {
"md5": "215d3a8171d6e8f2272f90ed4fd90166",
"sha256": "41b84fc9868067ece8fe49a474dbd92260bf27cbfb1a9586cf3f31fee17345c7"
},
"downloads": -1,
"filename": "rest_helpers-1.9.1-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "215d3a8171d6e8f2272f90ed4fd90166",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61173,
"upload_time": "2019-02-18T19:26:58",
"url": "https://files.pythonhosted.org/packages/76/f1/42e949539433e8735762aecb235055bbe987e4104f06113a02091dc1dfce/rest_helpers-1.9.1-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "2ad99bec5e07a491aff4dbc3cd159fb8",
"sha256": "3c4f5edad5698ec7f80273141d083a67f3efac001795c86add229ed32b60592a"
},
"downloads": -1,
"filename": "rest-helpers-1.9.1.tar.gz",
"has_sig": false,
"md5_digest": "2ad99bec5e07a491aff4dbc3cd159fb8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 67186,
"upload_time": "2019-02-18T19:26:59",
"url": "https://files.pythonhosted.org/packages/b7/bd/62e4c9750cc602056baccfa54e73973ad9ab0e2a52a98f65fd5c0efc31df/rest-helpers-1.9.1.tar.gz"
}
]
},
"urls": [
{
"comment_text": "",
"digests": {
"md5": "695a2853dd007a331d45f084040a35c4",
"sha256": "e7f0489ce64c0d28ad5e26e0571d964c3c3b9ae51339da5bc2fc4fc17f02b663"
},
"downloads": -1,
"filename": "rest_helpers-1.16-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "695a2853dd007a331d45f084040a35c4",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 61857,
"upload_time": "2019-07-11T20:06:04",
"url": "https://files.pythonhosted.org/packages/15/e3/fb7188b29f8e2489c5400d1a67a5a2f5e67eca073e145d36446f6d52b27d/rest_helpers-1.16-py2.py3-none-any.whl"
},
{
"comment_text": "",
"digests": {
"md5": "99c1d3a12dd50ae888224104ecce45d2",
"sha256": "acfd3adc5cc860373519bf409d05b8ae9a594f3bf67aab3a71f0348af23f0c53"
},
"downloads": -1,
"filename": "rest-helpers-1.16.tar.gz",
"has_sig": false,
"md5_digest": "99c1d3a12dd50ae888224104ecce45d2",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 68428,
"upload_time": "2019-07-11T20:06:06",
"url": "https://files.pythonhosted.org/packages/83/98/53460e030a5cf55855878c86d95bb618fe1cfb35788a17cbfd3461a9ca30/rest-helpers-1.16.tar.gz"
}
]
}