{ "info": { "author": "Managed by Q, Inc.", "author_email": "open-source@managedbyq.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development :: Libraries" ], "description": "# mbq.apitools\n\n`mbq.apitools` is 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.\n\nSome nice things about `mbq.apitools`:\n* All fields specified in a param or payload schema are required by default, and can be marked as optional by providing a `default=` 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.\n* The parsed parameters and payloads end up as rich types on the request. If a field is marked as `fields.DateTime`, then it will be on `request.payload` as a `datetime` object.\n* Pagination is handled entirely by the framework. Simply include `paginated=True` when you define the view, return a `PaginatedResponse`, and voila!\n* All success responses have a 200 status code.\n* All list and paginated responses contain the list of resources under the key `\"objects\"`.\n* All error responses have the same shape (an `\"error_code\"` and `\"detail\"` key).\n\n## Example\n\n```python\nfrom mbq.api_tools import fields, responses\nfrom mbq.api_tools.views import View, view\n\n@view(\n \"GET\",\n permissions=[SomeDRFPermissionClass],\n params={\n \"product_ids\": fields.Int(default=None, many=True),\n \"zipcode\": fields.String(),\n }\n)\ndef get_categories(request):\n categories = Category.objects.filter(zipcode=request.params.zipcode)\n\n if request.product_ids:\n categories = categories.filter(product_id__in=request.params.product_ids)\n\n categories = CategorySerializer(categories, many=True).data\n\n return responses.ListResponse(categories)\n\n\nclass OrdersView(View):\n\n @view.method(\"POST\", payload={\"company_id\": fields.Int()})\n def create_order(self, request):\n order = create_order(request.payload.company_id)\n return responses.DetailResponse(OrderSerializer(order).data))\n\n @view.method(\"GET\", permissions=[SomeDRFPermission], paginated=True)\n def get_orders(self, request):\n orders = Order.objects.all()\n return responses.PaginatedResponse(orders, OrderSerializer, request)\n```\n## Views\n\n### @view\n\nUse the `@view` decorator for all function based views. It accepts the following arguments:\n* `http_method_name`\n\t* GET, POST, PATCH, PUT, DELETE\n\t* Defaults to GET\n* `permissions`\n\t* A list of DRF permission classes _or_ functions that take in a request object and return `True` if authorized, `False` if unauthorized.\n* `params`\n\t* Schema for validating incoming query parameters. The query parameters will be available at `request.params`.\n* `payload`\n\t* Schema for validating incoming payloads. The payload will be available at `request.payload`.\n* `paginated`\n\t* Boolean indicating if the response will be paginated or not. Defaults to `False`.\n* `page_size`\n\t* Integer specifying the page size for a paginated response. This should only be used to override the default page size.\n* `on_unknown_field`\n\t* `\"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.\n\n### View\n\nUse the `View` class when you need to support multiple HTTP verbs for the same URL pattern.\nThe class exposes an `as_view()` method to use in `urls.py`.\n\nAll 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.)\n\n## Responses\n\nStatus codes are predefined by the different response classes. Status codes will _never_ be specified in a view.\n\n### Success Responses\n\nAll success responses return a 200 status code.\n\n#### `DetailResponse`\n\n```python\nresponses.DetailResponse({\"foo\": \"bar\", \"age\": 10})\n```\nwill generate:\n```json\n{\"foo\": \"bar\", \"age\": 10}\n```\n\n#### `ListResponse`\n`ListResponse` accepts any iterable of JSON serializable python objects and will nest them under an `\"objects\"` key in the response.\n```python\nresponses.ListResponse([{\"foo\": \"bar\"}])\n```\nwill generate:\n```json\n{\"objects\": [{\"foo\": \"bar\"}]}\n```\n\n#### `PaginatedResponse`\n`PaginatedResponse` accepts a `QuerySet`, 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.\n```python\nresponses.PaginatedResponse(some_queryset, SomeDRFSerializer, request)\n```\nwill generate:\n```\n{\n \"objects\": [{id: 1}, {id: 2}, {id: 3}, ... ],\n \"pagination\": {\n \"page\": 1,\n \"page_size\": 20,\n \"num_pages\": 5,\n \"total_objects\": 89,\n \"next_page\": \"/api/v1/orders?page=2&page_size=20\",\n \"previous_page\": null\n }\n}\n```\nAlternatively, 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,\n```python\nresponses.PaginatedResponse(\n some_queryset,\n lambda objs: [obj.to_dict() for obj in objs],\n request,\n)\n```\nSee the `Pagination` section for more details.\n\n### Error Responses\n\nAll error responses will have the following shape:\n```json\n{\"error_code\": \"some_unique_error_string\", \"detail\": \"More details about the error...\"}\n```\nSome allow the `error_code` and `detail` to be specified, while others have them hard-coded.\n\n#### `ClientErrorResponse`\n* 400\n* To be used when the client made an error it could have avoided.\n* `error_code` and `detail` must be specified.\n```python\nresponses.ClientErrorResponse(\"quote_state_error\", \"Cannot approve an already approved quote\")\n```\n\n#### `UnauthenticatedResponse`\n* 401\n* `error_code`\n * `\"unauthenticated\"`\n* `detail`\n * `\"Authentication credentials were not provided\"`\n\n#### `UnauthorizedResponse`\n* 403\n* `error_code`\n\t* `\"unauthorized\"`\n* `detail`\n\t* `\"Unauthorized to access this resource\"`\n\n#### `NotFoundResponse`\n* 404\n* `error_code`\n\t* `\"not_found\"`\n* `detail`\n\t* `\"Resource not found\"`\n\n#### `ServerValidationErrorResponse`\n* 422\n* To be used when a validation error occurs that could only be detected by the server.\n* `error_code` and `detail` must be specified.\n ```python\nresponses.ClientErrorResponse(\"email_already_taken\", \"The email you have provided is already in use\")\n```\n\n#### `TooManyRequestsResponse`\n* 429\n* `error_code`\n * `\"too_many_requests\"`\n```python\nresponses.TooManyRequestsResponse(detail=\"Too many requests for template\")\n```\n\n#### `ServerErrorResponse`\n* 500\n* `error_code`\n * `\"server_error\"`\n* `detail`\n * `\"An unexpected error occurred\"`\n\n## Exceptions\n\n### ValidationError\nUse sparingly. If raised, the framework will catch it and return generate a `ClientErrorResponse`, with `\"validation_error\"` as the `error_code` and the error message as `detail`. However, in most cases you should just catch your own errors and return `ClientErrorResponse`.\n\n### ImmediateResponseError\nThis 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.\n```python\nclass SomeObjView(View):\n def _get_some_obj(self, id):\n try:\n return SomeObj.objects.get(id=id)\n except:\n raise exceptions.ImmediateResponseError(responses.NotFoundResponse())\n\n @view.method(\"GET\")\n def get_some_obj(request, id=None):\n obj = self._get_some_obj(id)\n ...\n\n @view.method(\"PATCH\")\n def patch_some_obj(request, id=None):\n obj = self._get_some_obj(id)\n ...\n```\n## Schemas & Fields\n\nSchemas and Fields use the Marshmallow library under the hood.\n\nAll schema fields support the following arguments:\n* `default`\n\t* All fields in a schema are required by default. Use the `default` argument to both mark a field as optional and specify the default value to use if the field is not received.\n\t* If you would like the field to be left out entirely of the parsed params/payload, you can do `default=fields.OMIT`\n* `allow_none`\n\t* Defaults to `False`. Pass in `True` if you would like to accept `None` (`null`) as a passed in value.\n* `validate`\n\t* Function that takes in the value and returns `True` or `False` appropriately.\n* `transform`\n\t* Function that takes in the value and returns a value of the same type. This will run _before_ validation.\n* `many`\n\t* Defaults to `False`. Pass in `True` if multiple values are allowed for the field. `many=True` will result in a list of values on the parsed params/payload.\n\t* 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)`.\n\t* For payloads, a list of values is expected.\n\t* When used with `validate` and/or `transform`, these functions will be applied to each individual value.\n* `param_name`\n\t* Use 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 be `order_state` under `request.params`, you would do: `{\"order_state\": fields.String(param_name=\"state\")}`\n\nThe available fields and their custom arguments are:\n* `Bool`\n* `DateTime`\n* `Date`\n* `Time`\n* `Int`\n\t* `min_val`\n\t* `max_val`\n* `String`\n\t* `min_length`\n\t* `max_length`\n\t* `allow_empty`\n\t\t* Defaults to `False`. If you would like to accept empty strings as valid values passed in, do `allow_empty=True`.\n* `Enum`\n\t* `choices`\n\t\t* List of acceptable string values\n* `UUID`\n* `Float`\n* `Decimal`\n\t* `max_digits`\n * `min_val`\n * `max_val`\n* `Email`\n* `Dict`\n\t* Use sparingly. Allows for any arbitrary dictionary to be passed in.\n\n### Nested schemas\n\nTo nest a schema, do:\n```python\n@view(payload={\n \"id\": fields.Int(),\n \"nested_object\": fields.Nested({\n \"name\": fields.String()\n })\n})\n```\n`Nested` fields accept all of the same arguments as the other fields.\n\n### What about PATCH?\n\nIf 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 like `default=None` is 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 pass `default=fields.OMIT` to the field and it will be left out of the parsed data if it wasn't sent down. Here's an example:\n```python\n@view(\n \"PATCH\",\n payload={\n \"first_name\": fields.String(default=fields.OMIT),\n \"last_name\": fields.String(default=fields.OMIT)\n }\n)\ndef update_person(request, id=None):\n person = Person.objects.get(id=id)\n\n data = request.payload.as_dict()\n\n if \"first_name\" in data:\n person.first_name = data[\"first_name\"]\n if \"last_name\" in data:\n person.last_name = data[\"last_name\"]\n\n person.save()\n\n return responses.DetailResponse()\n\n```\n\n## Pagination\n\nThe pagination mechanics are fully managed by the framework. All you have to do is specify `paginated=True` in the decorator, and return a `PaginatedResponse` object.\n\nThe expected query parameters when `paginated=True` are `page` and `page_size`.\n\n`page` obviously defaults to `1`, and `page_size` defaults to `20`, but the client is more than welcome to send down a different value for `page_size`.\n\nYou can globally override the default `page_size` via the `API_TOOLS[\"DEFAULT_PAGE_SIZE\"]` setting. You can override a particular view's default page size via the `page_size` argument to the decorator.\n\nThe pagination data returned in the response under the `pagination` key looks like:\n```json\n{\n \"page\": 1,\n \"page_size\": 20,\n \"num_pages\": 5,\n \"total_objects\": 89,\n \"next_page\": \"/api/v1/orders?page=2&page_size=20\",\n \"previous_page\": null\n}\n```\nAn 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.\n\n## Requests\n\nIf `params` or `payload` is specified in the decorator, then the parsed query parameters will be at `request.params` or `request.payload`. This will be an immutable object. If you would like it in the form of a dictionary, you can do `request.params.as_dict()`.\n\n## Permissions\n\nPermissions are required. A non-empty list must be passed into the decorator. If you would like to write an unauthorized endpoint, you can do:\n```python\nfrom mbq.api_tools import permissions\n\n@view(permissions=[permissions.NoAuthorization])\ndef my_view(request):\n pass\n````\n\n## Settings\nThe default settings are:\n```python\nAPI_TOOLS = {\n \"DEFAULT_PAGE_SIZE\": 20,\n \"UNKNOWN_PAYLOAD_FIELDS\": \"raise\",\n \"UNKNOWN_PARAM_FIELDS\": \"raise\",\n}\n```\nOverride as you see fit.", "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/managedbyq/mbq.apitools", "keywords": "", "license": "Apache 2.0", "maintainer": "Managed by Q, Inc.", "maintainer_email": "open-source@managedbyq.com", "name": "mbq.apitools", "package_url": "https://pypi.org/project/mbq.apitools/", "platform": "", "project_url": "https://pypi.org/project/mbq.apitools/", "project_urls": { "Homepage": "https://github.com/managedbyq/mbq.apitools" }, "release_url": "https://pypi.org/project/mbq.apitools/1.5.1/", "requires_dist": null, "requires_python": "", "summary": "MBQ APITools", "version": "1.5.1" }, "last_serial": 5782182, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "d73d484360958ecda0db739765785950", "sha256": "eb39393f01ad6363319ca4bc8618f04b50c61fe45f49804b746fbbb6e36b4a51" }, "downloads": -1, "filename": "mbq.apitools-0.0.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "d73d484360958ecda0db739765785950", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 22975, "upload_time": "2019-07-23T16:10:17", "url": "https://files.pythonhosted.org/packages/2a/fa/8e0b944046287759b908dffa75a605da648801d06f3f01a77b461e1ecaad/mbq.apitools-0.0.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5423d54b90e51d1fce3fa02c8f637847", "sha256": "a7cc5ff7ef3b565d9fb0538e706d1e6e935e4f2f9f474bf83bd499afadb3b732" }, "downloads": -1, "filename": "mbq.apitools-0.0.1.tar.gz", "has_sig": false, "md5_digest": "5423d54b90e51d1fce3fa02c8f637847", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14419, "upload_time": "2019-07-23T16:10:19", "url": "https://files.pythonhosted.org/packages/00/4d/3c2f53fa20dd3d8a90eb214e40590c5949fb29cd3ff8762ce4614ab50b12/mbq.apitools-0.0.1.tar.gz" } ], "0.0.4": [ { "comment_text": "", "digests": { "md5": "9f96a989c676f4d9e57801935fad434b", "sha256": "cd6e90e7a571a7073bd4057899b35d1d605d92528acfa86b5c9ac90035753fb7" }, "downloads": -1, "filename": "mbq.apitools-0.0.4.tar.gz", "has_sig": false, "md5_digest": "9f96a989c676f4d9e57801935fad434b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14387, "upload_time": "2019-07-23T18:01:20", "url": "https://files.pythonhosted.org/packages/36/07/805f661901972ded67a557aed743d9055b92b42fbd2856062bf2c6c6a6df/mbq.apitools-0.0.4.tar.gz" } ], "1.0.0": [ { "comment_text": "", "digests": { "md5": "9318f2efa27de689cee7d11293b6bf02", "sha256": "c531cafc2023f811f1097a7784ce8afcc2fbfcae031ece85ea0dd6a86c0be991" }, "downloads": -1, "filename": "mbq.apitools-1.0.0.tar.gz", "has_sig": false, "md5_digest": "9318f2efa27de689cee7d11293b6bf02", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14382, "upload_time": "2019-07-23T20:58:09", "url": "https://files.pythonhosted.org/packages/cb/a1/2e23727f322a348d8d413ebdfc6478e87ff4e26e3eb8b175807f022304ac/mbq.apitools-1.0.0.tar.gz" } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "dcf9d1c7accbb57fa68072f5e530dffc", "sha256": "6b725b6edeb5f306f9957a9d09f785cdeec7d548f28255bb74b1c9c0fbc053ee" }, "downloads": -1, "filename": "mbq.apitools-1.1.0.tar.gz", "has_sig": false, "md5_digest": "dcf9d1c7accbb57fa68072f5e530dffc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14643, "upload_time": "2019-07-24T15:57:58", "url": "https://files.pythonhosted.org/packages/d4/f8/a757d4fe7d1cb2a154649ec40e84ef70c5f172afde68b8e26abde01fa15d/mbq.apitools-1.1.0.tar.gz" } ], "1.1.1": [ { "comment_text": "", "digests": { "md5": "e0e0ae02689981bdf2a84788b6bd4122", "sha256": "a393231e1983eace82e7c5a4a22dbeadedceb5fbbe05557fc18c32f48100f429" }, "downloads": -1, "filename": "mbq.apitools-1.1.1.tar.gz", "has_sig": false, "md5_digest": "e0e0ae02689981bdf2a84788b6bd4122", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14689, "upload_time": "2019-07-24T18:11:39", "url": "https://files.pythonhosted.org/packages/ab/63/9ee01d0a232094a3347345c4c96695375243ca555ef458c121110261a1bb/mbq.apitools-1.1.1.tar.gz" } ], "1.1.2": [ { "comment_text": "", "digests": { "md5": "525d5fc7fbe6a494dbcc756357ae1c25", "sha256": "9080047ac6254578f35236da030b5f287542644706a430a2eb430a554a753b09" }, "downloads": -1, "filename": "mbq.apitools-1.1.2.tar.gz", "has_sig": false, "md5_digest": "525d5fc7fbe6a494dbcc756357ae1c25", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23637, "upload_time": "2019-07-25T16:25:01", "url": "https://files.pythonhosted.org/packages/05/62/eff0ff8128c5e599391cbeed8285379e80c5224ff75687030be65b9be0ed/mbq.apitools-1.1.2.tar.gz" } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "d0afb5d07dbc39f6de63d587fb924509", "sha256": "eee2d18c2b5613b9ff7c4e792f2002818fa1f88b97ceba9eac5b67b0b3c8dedf" }, "downloads": -1, "filename": "mbq.apitools-1.2.1.tar.gz", "has_sig": false, "md5_digest": "d0afb5d07dbc39f6de63d587fb924509", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23629, "upload_time": "2019-07-25T18:01:18", "url": "https://files.pythonhosted.org/packages/0e/2e/ad4bbfaad7347ddeb8cdbab3572f72388dcef50d5dc6bec3f8035b657b7b/mbq.apitools-1.2.1.tar.gz" } ], "1.2.2": [ { "comment_text": "", "digests": { "md5": "80f3ae1b9bdbfc434c1fff6668499905", "sha256": "457b4d50d24e7c2d647e8c11f76f5986700bf79517bbfa9893dc6014042e5d9d" }, "downloads": -1, "filename": "mbq.apitools-1.2.2.tar.gz", "has_sig": false, "md5_digest": "80f3ae1b9bdbfc434c1fff6668499905", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23639, "upload_time": "2019-07-25T18:18:08", "url": "https://files.pythonhosted.org/packages/0f/11/2e7dca84b71aeb7a2d3c8b2dcbb0a9edf1538d294a555662b0b466e88fbd/mbq.apitools-1.2.2.tar.gz" } ], "1.2.3": [ { "comment_text": "", "digests": { "md5": "9dff6639613bf7b235d13ffc52962fca", "sha256": "9daceff4a6efb18da6378735d2d5b6c53f94ef788903ecc84c177d56dcc8b4eb" }, "downloads": -1, "filename": "mbq.apitools-1.2.3.tar.gz", "has_sig": false, "md5_digest": "9dff6639613bf7b235d13ffc52962fca", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23700, "upload_time": "2019-07-25T19:00:56", "url": "https://files.pythonhosted.org/packages/3d/9a/b8b7449e97ca842ed96a41482950737e7a9e57c868698a8b8c85c9200755/mbq.apitools-1.2.3.tar.gz" } ], "1.3.0": [ { "comment_text": "", "digests": { "md5": "a01ca08c26d5e0dea07171c17d3397dc", "sha256": "d36621cc64093248dc88f2f6a5f4fd829310a5ea6ad85e4b54c4c72d6164af4d" }, "downloads": -1, "filename": "mbq.apitools-1.3.0.tar.gz", "has_sig": false, "md5_digest": "a01ca08c26d5e0dea07171c17d3397dc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23891, "upload_time": "2019-07-25T21:06:30", "url": "https://files.pythonhosted.org/packages/72/e6/6b0fcbaf96ee39f1b0d5344a13691d7da1905b84b8f69396b131f93b32fa/mbq.apitools-1.3.0.tar.gz" } ], "1.3.1": [ { "comment_text": "", "digests": { "md5": "fc6e56a6855f431d66271ad962e6463d", "sha256": "59dc09fb679b2201e7a16625978bd380157663fba0271933da6d6ebcabe00bdd" }, "downloads": -1, "filename": "mbq.apitools-1.3.1.tar.gz", "has_sig": false, "md5_digest": "fc6e56a6855f431d66271ad962e6463d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23911, "upload_time": "2019-07-26T17:17:14", "url": "https://files.pythonhosted.org/packages/8a/65/242a44e916d17036c43a8b5bf43ae9d24b7ef25fb0e16ccecbf681dc4a8e/mbq.apitools-1.3.1.tar.gz" } ], "1.3.2": [ { "comment_text": "", "digests": { "md5": "b108d61058a89efa1cb98b7ccb5c0fbd", "sha256": "633373581e55327172f5b8f0116038a43fd1b61ebf8c6d4a0f189e853f2764eb" }, "downloads": -1, "filename": "mbq.apitools-1.3.2.tar.gz", "has_sig": false, "md5_digest": "b108d61058a89efa1cb98b7ccb5c0fbd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23957, "upload_time": "2019-07-29T16:25:47", "url": "https://files.pythonhosted.org/packages/bb/27/f9684b0dee3642b97b44bca823643ef2452d7f59fb6e72a7f61bcbb90f36/mbq.apitools-1.3.2.tar.gz" } ], "1.4.0": [ { "comment_text": "", "digests": { "md5": "578bf84315166ddc72f5c9a0911e2546", "sha256": "f8c873a1f2ae257062b79f094c2a314487e3bb4bb985a2f1f4f2e312b0da5154" }, "downloads": -1, "filename": "mbq.apitools-1.4.0.tar.gz", "has_sig": false, "md5_digest": "578bf84315166ddc72f5c9a0911e2546", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24157, "upload_time": "2019-07-29T20:41:39", "url": "https://files.pythonhosted.org/packages/c8/ae/12711ae8726b6f2cfb07867648c6b14fe4c8b1c0db0998e353989a30c208/mbq.apitools-1.4.0.tar.gz" } ], "1.4.1": [ { "comment_text": "", "digests": { "md5": "801f5d999fc5ebd34d81a76c28d7ef6b", "sha256": "d595bada69ecf62791669a8cabb0b61a21e6154b181b928e9c83905c0a3f1915" }, "downloads": -1, "filename": "mbq.apitools-1.4.1.tar.gz", "has_sig": false, "md5_digest": "801f5d999fc5ebd34d81a76c28d7ef6b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25774, "upload_time": "2019-07-30T17:49:09", "url": "https://files.pythonhosted.org/packages/d2/f4/d046d92e28c48d1313a3db15746e1b98bb3a9f5df7743fb3feaefe37c031/mbq.apitools-1.4.1.tar.gz" } ], "1.4.2": [ { "comment_text": "", "digests": { "md5": "325ea5f689ebfa94703734b840bcae2c", "sha256": "6863f5a60f78022eb128fd20a44e1e83dc92cccab69eda6e94557389d4c7ba25" }, "downloads": -1, "filename": "mbq.apitools-1.4.2.tar.gz", "has_sig": false, "md5_digest": "325ea5f689ebfa94703734b840bcae2c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25903, "upload_time": "2019-07-30T19:58:16", "url": "https://files.pythonhosted.org/packages/b5/74/ad347b52984bcdfffa4c83d1324c3562a249216e7fc5ca523f034b534821/mbq.apitools-1.4.2.tar.gz" } ], "1.4.3": [ { "comment_text": "", "digests": { "md5": "7f937f69814b9073b03a3950d5dc3adb", "sha256": "34fc4ab1e067dfdfaae2a89d673fa5e64f1bc3b332abc024ac25237a48444c92" }, "downloads": -1, "filename": "mbq.apitools-1.4.3.tar.gz", "has_sig": false, "md5_digest": "7f937f69814b9073b03a3950d5dc3adb", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26111, "upload_time": "2019-08-16T14:33:39", "url": "https://files.pythonhosted.org/packages/b6/3a/ec339f1d04e41eab6237f28f8f2eea6dbfd4cb56c993a4a68b1579cf1d6e/mbq.apitools-1.4.3.tar.gz" } ], "1.5.1": [ { "comment_text": "", "digests": { "md5": "07ad8275a5aaa2237e30bc4727984f00", "sha256": "702ee0bb18272b3bb052abfe4a1d42f4ae5028ffe9e7c2353a31649dbdd06fdd" }, "downloads": -1, "filename": "mbq.apitools-1.5.1.tar.gz", "has_sig": false, "md5_digest": "07ad8275a5aaa2237e30bc4727984f00", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26226, "upload_time": "2019-09-04T16:02:23", "url": "https://files.pythonhosted.org/packages/c4/dc/0c63dc3b285f0275cbcef1e5bb4dbab726f016694e5fa45663fceefb9c31/mbq.apitools-1.5.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "07ad8275a5aaa2237e30bc4727984f00", "sha256": "702ee0bb18272b3bb052abfe4a1d42f4ae5028ffe9e7c2353a31649dbdd06fdd" }, "downloads": -1, "filename": "mbq.apitools-1.5.1.tar.gz", "has_sig": false, "md5_digest": "07ad8275a5aaa2237e30bc4727984f00", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26226, "upload_time": "2019-09-04T16:02:23", "url": "https://files.pythonhosted.org/packages/c4/dc/0c63dc3b285f0275cbcef1e5bb4dbab726f016694e5fa45663fceefb9c31/mbq.apitools-1.5.1.tar.gz" } ] }