{ "info": { "author": "LAZR Developers", "author_email": "lazr-users@lists.launchpad.net", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", "Programming Language :: Python" ], "description": "..\n This file is part of lazr.batchnavigator.\n\n lazr.batchnavigator is free software: you can redistribute it and/or\n modify it under the terms of the GNU Lesser General Public License as\n published by the Free Software Foundation, version 3 of the License.\n\n lazr.batchnavigator is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public\n License for more details.\n\n You should have received a copy of the GNU Lesser General Public License\n along with lazr.batchnavigator. If not, see\n .\n\nBatch Navigation\n****************\n\nBatch navigation provides a way to navigate batch results in a web\npage by providing URL links to the next, previous and numbered pages\nof results.\n\nIt uses four query/POST arguments to control the batching:\n\n - memo: A record of the underlying storage index pointer for the position of\n the batch.\n - direction: Indicates whether the memo is at the start or end of the batch.\n - start: Cosmetic - used to calculate the apparent location (but note that\n due to the concurrent nature of repeated visits to batches that the\n true offset may differ - however the collection won't skip or show\n items twice. For compatibility with saved URLs, if memo and\n direction are both missing then start is used to do list slicing\n into the collection.\n - batch: Controls the amount of items we are showing per batch. It will only\n appear if it's different from the default value set when the batch\n is created.\n\nThese values can be overriden in the request, unless you also pass\nforce_start=True, which will make the start argument (again, defaulting to 0)\nalways chosen.\n\nImports:\n\n >>> from lazr.batchnavigator import BatchNavigator, ListRangeFactory\n >>> from zope.publisher.browser import TestRequest\n >>> from zope.publisher.http import HTTPCharsets\n >>> from zope.component import getSiteManager\n >>> sm = getSiteManager()\n >>> sm.registerAdapter(HTTPCharsets)\n\n >>> def build_request(query_string_args=None, method='GET'):\n ... if query_string_args is None:\n ... query_string = ''\n ... else:\n ... if getattr(query_string_args, 'items', None) is not None:\n ... query_string_args = query_string_args.items()\n ... query_string = \"&\".join(\n ... [\"%s=%s\" % (k,v) for k,v in query_string_args])\n ... request = TestRequest(SERVER_URL='http://www.example.com/foo',\n ... method=method,\n ... environ={'QUERY_STRING': query_string})\n ... request.processInputs()\n ... return request\n\nA dummy request object:\n\nSome sample data.\n\n >>> reindeer = ['Dasher', 'Dancer', 'Prancer', 'Vixen', 'Comet',\n ... 'Cupid', 'Donner', 'Blitzen', 'Rudolph']\n\nBecause slicing large collections can be very expensive, BatchNavigator offers\na non-slice protocol for determining the edge of batches. The range_factory\nsupplies an object implementing IRangeFactory and manages this protocol.\nListRangeFactory is a simple included implementation which BatchNavigator will\nuse if no range_factory is supplied.\n\n >>> _ = BatchNavigator(reindeer, build_request(),\n ... range_factory=ListRangeFactory(reindeer))\n\nFor the examples in the documentation we let BatchNavigator construct a\nrange_factory implicitly:\n\n >>> safe_reindeer = reindeer\n >>> safe_reindeer_batch_navigator = BatchNavigator(\n ... safe_reindeer, build_request(), size=3)\n\nAn important feature of lazr.batchnavigator is its reluctance to\ninvoke len() on an underlying data set. len() can be an expensive\noperation that provides little benefit, so this library tries hard to\navoid calling len() unless it's absolutely necessary. To show this\noff, we'll define a subclass of Python's list type that explodes when\nlen() is invoked on it.\n\n >>> class ListWithExplosiveLen(list):\n ... \"\"\"A list subclass that doesn't like its len() being called.\"\"\"\n ... def __len__(self):\n ... raise RuntimeError\n\nUnless otherwise stated, we will use this list exclusively throughout\nthis test, to verify that len() is never called unless we want it to\nbe.\n\n >>> explosive_reindeer = ListWithExplosiveLen(reindeer)\n >>> reindeer_batch_navigator = BatchNavigator(\n ... explosive_reindeer, build_request(), size=3)\n\nThe BatchNavigator implements IBatchNavigator. We need to use the\n'safe' batch navigator here, because verifyObject probes all methods\nof the object it's passed, including __len__.\n\n >>> from zope.interface.verify import verifyObject\n >>> from lazr.batchnavigator.interfaces import IBatchNavigator\n\n >>> verifyObject(IBatchNavigator, safe_reindeer_batch_navigator)\n True\n\nThe BatchNavigator class provides IBatchNavigatorFactory. This can be used\nto register a batch navigator factory as a utility, for instance.\n\n >>> from lazr.batchnavigator.interfaces import IBatchNavigatorFactory\n\n >>> verifyObject(IBatchNavigatorFactory, BatchNavigator)\n True\n\nYou can ask the navigator for the chunk of results currently being shown\n(e.g. to iterate over them for rendering in ZPT):\n\n >>> list(reindeer_batch_navigator.currentBatch())\n ['Dasher', 'Dancer', 'Prancer']\n\nYou can ask for the first, previous, next and last results' links:\n\n >>> reindeer_batch_navigator.firstBatchURL()\n ''\n >>> reindeer_batch_navigator.prevBatchURL()\n ''\n >>> reindeer_batch_navigator.nextBatchURL()\n 'http://www.example.com/foo?memo=3&start=3'\n\nThere's no way to get the URL to the final batch without knowing the\nlength of the entire list, so we'll use the safe batch navigator to\ndemonstrate lastBatchURL():\n\n >>> safe_reindeer_batch_navigator.lastBatchURL()\n 'http://www.example.com/foo?direction=backwards&start=6'\n\nThe next link will be empty when there are no further results:\n\n >>> request = build_request({\"start\": \"3\", \"batch\": \"20\"})\n >>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)\n >>> last_reindeer_batch_navigator.nextBatchURL()\n ''\n\nThe first and previous link should appear even when we start at a point between 0\nand the batch size:\n\n >>> request = build_request({\"start\": \"2\", \"batch\": \"3\"})\n >>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)\n\nHere, we can see too that the batch argument appears as part of the URL.\nThat's because the request asked for a different size than the default\none when we create the Batch object, by default, it's 5.\n\n >>> last_reindeer_batch_navigator.firstBatchURL()\n 'http://www.example.com/foo?batch=3'\n\n >>> last_reindeer_batch_navigator.prevBatchURL()\n 'http://www.example.com/foo?batch=3&direction=backwards&memo=2'\n\nThis all works with other values in the query string, too:\n\n >>> request = build_request({'fnorb': 'bar',\n ... 'start': '3',\n ... 'batch': '3'})\n >>> reindeer_batch_navigator_with_qs = BatchNavigator(\n ... reindeer, request, size=3)\n >>> safe_reindeer_batch_navigator_with_qs = BatchNavigator(\n ... safe_reindeer, request, size=3)\n\n\nIn this case, we created the BatchNavigator with a default size of '3' and\nthe request is asking exactly that number of items per batch, and thus, we\ndon't need to show 'batch' as part of the URL.\n\n >>> reindeer_batch_navigator_with_qs.firstBatchURL()\n 'http://www.example.com/foo?fnorb=bar'\n >>> reindeer_batch_navigator_with_qs.prevBatchURL()\n 'http://www.example.com/foo?fnorb=bar&direction=backwards&memo=3'\n >>> reindeer_batch_navigator_with_qs.nextBatchURL()\n 'http://www.example.com/foo?fnorb=bar&memo=6&start=6'\n\n(Again, there's no way to get the last batch without knowing the size\nof the entire list.)\n\n >>> safe_reindeer_batch_navigator_with_qs.lastBatchURL()\n 'http://www.example.com/foo?fnorb=bar&direction=backwards&start=6'\n\nThe ``force_start`` argument allows you to ignore the start value in the\nrequest. This can be useful when, for instance, a filter has changed, and the\ndesired behavior is to restart at 0.\n\n >>> reindeer_batch_navigator_with_qs = BatchNavigator(\n ... reindeer, request, size=3, force_start=True)\n >>> reindeer_batch_navigator_with_qs.currentBatch().start\n 0\n >>> reindeer_batch_navigator_with_qs.nextBatchURL()\n 'http://www.example.com/foo?fnorb=bar&memo=3&start=3'\n >>> reindeer[:3] == list(reindeer_batch_navigator_with_qs.currentBatch())\n True\n\nWe ensure that batch arguments supplied in the URL are observed\nfor POST operations too:\n\n >>> request = build_request({'fnorb': 'bar',\n ... 'start': '3',\n ... 'batch': '3'}, method='POST')\n >>> reindeer_batch_navigator_post_with_qs = BatchNavigator(\n ... reindeer, request)\n\n >>> reindeer_batch_navigator_post_with_qs.start\n 3\n >>> reindeer_batch_navigator_post_with_qs.nextBatchURL()\n 'http://www.example.com/foo?fnorb=bar&batch=3&memo=6&start=6'\n\nWe ensure that multiple size and batch arguments supplied in the URL don't\nblow up the application. The first one is preferred.\n\n >>> request = build_request(\n ... [('batch', '1'), ('batch', '7'), ('start', '2'), ('start', '10')])\n >>> navigator = BatchNavigator(reindeer, request=request)\n >>> navigator.nextBatchURL()\n 'http://www.example.com/foo?batch=1&memo=3&start=3'\n\nThe batch argument must be positive. Other numbers are ignored, and the\ndefault batch size is used instead.\n\n >>> from cgi import parse_qs\n >>> request = build_request({'batch': '0'})\n >>> navigator = BatchNavigator(range(99), request=request)\n >>> print 'batch' in parse_qs(navigator.nextBatchURL())\n False\n\n >>> request = build_request({'batch': '-1'})\n >>> navigator = BatchNavigator(range(99), request=request)\n >>> print 'batch' in parse_qs(navigator.nextBatchURL())\n False\n\n\n=============\nEmpty Batches\n=============\n\nYou can also create an empty batch that will not have any items:\n\n >>> null_batch_navigator = BatchNavigator(\n ... None, build_request(), size=3)\n >>> null_batch_navigator.firstBatchURL()\n ''\n >>> null_batch_navigator.nextBatchURL()\n ''\n >>> null_batch_navigator.prevBatchURL()\n ''\n >>> null_batch_navigator.lastBatchURL()\n ''\n\n >>> null_batch_navigator = BatchNavigator(\n ... [], build_request(), size=3)\n >>> null_batch_navigator.firstBatchURL()\n ''\n >>> null_batch_navigator.nextBatchURL()\n ''\n >>> null_batch_navigator.prevBatchURL()\n ''\n >>> null_batch_navigator.lastBatchURL()\n ''\n\nTODO:\n\n - blowing up when start is beyond end\n - orphans\n - overlap\n\n====================================\nSupporting Results Without a __len__\n====================================\n\nSome result objects do not implement __len__ because generally Python code\nassumes that __len__ is cheap. SQLObject and Storm result sets both have this\nbehavior, for instance, so that it is cleat that getting the length is a non-\ntrivial operation.\n\nTo support these objects, the batch looks for __len__ on the result set. If\nit does not exist, it adapts the result to\nzope.interface.common.sequence.IFiniteSequence and uses that __len__.\n\n >>> class ExampleResultSet(object):\n ... def __init__(self, results):\n ... self.stub_results = results\n ... def count(self):\n ... # imagine this actually returned\n ... return len(self.stub_results)\n ... def __getitem__(self, ix):\n ... return self.stub_results[ix] # also works with slices\n ... def __iter__(self):\n ... return iter(self.stub_results)\n ...\n >>> from zope.interface import implements\n >>> from zope.component import adapts, getSiteManager\n >>> from zope.interface.common.sequence import IFiniteSequence\n >>> class ExampleAdapter(ExampleResultSet):\n ... adapts(ExampleResultSet)\n ... implements(IFiniteSequence)\n ... def __len__(self):\n ... return self.stub_results.count()\n ...\n >>> sm = getSiteManager()\n >>> sm.registerAdapter(ExampleAdapter)\n >>> example = ExampleResultSet(safe_reindeer)\n >>> example_batch_navigator = BatchNavigator(\n ... example, build_request(), size=3)\n >>> example_batch_navigator.currentBatch().total()\n 9\n\n========================\nOnly Gets What Is Needed\n========================\n\nIt's also important for performance of batching large result sets that the\nbatch only gets a slice of the results, rather than accessing the entirety.\n\n >>> class ExampleResultSet(ExampleResultSet):\n ... def __init__(self, results):\n ... super(ExampleResultSet, self).__init__(results)\n ... self.getitem_history = []\n ... def __getitem__(self, ix):\n ... self.getitem_history.append(ix)\n ... return super(ExampleResultSet, self).__getitem__(ix)\n ...\n\n >>> example = ExampleResultSet(reindeer)\n >>> example_batch_navigator = BatchNavigator(\n ... example, build_request(), size=3)\n >>> reindeer[:3] == list(example_batch_navigator.currentBatch())\n True\n >>> example.getitem_history\n [slice(0, 4, None)]\n\nNote that although the batch is of the size requested, the underlying\nlist contains one more item than is necessary. This is to make it easy\nto determine whether a given batch is the final one in the list,\nwithout having to explicitly look up the length of the list\n(potentially an expensive operation).\n\n\n=========================\nAdding callback functions\n=========================\n\nSometimes it is useful to have a function called with the batched\nvalues once they have been determined. This is the case when there\nare subsequent queries that are needed to be executed for each batch,\nand it is undesirable or overly expensive to execute the query for\nevery value in the entire result set.\n\nThe callback function must define two parameters. The first is the\nbatch navigator object itself, and the second it the current batch.\nThe callback function is called once and only once when the\nBatchNavigator is constructed, and the current batch is determined.\n\n >>> def print_callback(context, batch):\n ... for item in batch:\n ... print item\n\n >>> reindeer_batch_navigator = BatchNavigator(\n ... reindeer, build_request(), size=3, callback=print_callback)\n Dasher\n Dancer\n Prancer\n\n >>> request = build_request({\"start\": \"3\", \"batch\": \"20\"})\n >>> last_reindeer_batch_navigator = BatchNavigator(\n ... reindeer, request=request, callback=print_callback)\n Vixen\n Comet\n Cupid\n Donner\n Blitzen\n Rudolph\n\nMost likely, the callback function will be bound to a view class.\nBy providing the batch navigator itself as the context for the\ncallback allows the addition of extra member variables. This is\nuseful as the BatchNavigator becomes the context in page templates\nthat are batched.\n\n >>> class ReindeerView:\n ... def constructReindeerFromAtoms(self, context, batch):\n ... # some significantly slow process\n ... view.built_reindeer = list(batch)\n ... def batchedReindeer(self):\n ... return BatchNavigator(\n ... reindeer, build_request(), size=3,\n ... callback=self.constructReindeerFromAtoms)\n\n >>> view = ReindeerView()\n >>> batch_navigator = view.batchedReindeer()\n >>> print view.built_reindeer\n ['Dasher', 'Dancer', 'Prancer']\n >>> print list(batch_navigator.currentBatch())\n ['Dasher', 'Dancer', 'Prancer']\n\n==================\nMaximum batch size\n==================\n\nSince the batch size is exposed in the URL, it's possible for users to\ntweak the batch parameter to retrieve more results. Since that may\npotentially exhaust server resources, an upper limit is put on the batch\nsize. If the requested batch parameter is higher than this, an\nInvalidBatchSizeError is raised.\n\n >>> class DemoBatchNavigator(BatchNavigator):\n ... max_batch_size = 5\n ...\n >>> request = build_request({\"start\": \"0\", \"batch\": \"20\"})\n >>> DemoBatchNavigator(reindeer, request=request )\n Traceback (most recent call last):\n ...\n InvalidBatchSizeError: Maximum for \"batch\" parameter is 5.\n\n==============\nURL parameters\n==============\n\nNormally, any parameters passed in the current page's URL are\nreproduced in the batch navigator's links. A \"transient\" parameter is\none that was only relevant for the current page request and shouldn't be\npassed on to subsequent ones.\n\nIn this next batch navigator, two parameters occur in the page's URL:\n\"noisy\" and \"quiet.\"\n\n >>> request_parameters = {\n ... 'quiet': 'ssht',\n ... 'noisy': 'HELLO',\n ... }\n\n >>> request_with_parameters = build_request(request_parameters)\n\nOne parameter, \"quiet,\" is transient. There is another transient\nparameter called \"absent,\" but it's not passed in our ongoing page\nrequest.\n\n >>> def build_navigator(list):\n ... return BatchNavigator(\n ... list, request_with_parameters, size=3,\n ... transient_parameters=['quiet', 'absent'])\n >>> navigator_with_parameters = build_navigator(reindeer)\n >>> safe_navigator_with_parameters = build_navigator(safe_reindeer)\n\nOf these three parameters, only \"noisy\" recurs in the links produced by\nthe batch navigator.\n\n >>> navigator_with_parameters.nextBatchURL()\n 'http://www.example.com/foo?noisy=HELLO&memo=3&start=3'\n >>> safe_navigator_with_parameters.lastBatchURL()\n 'http://www.example.com/foo?noisy=HELLO&direction=backwards&start=6'\n\nThe transient parameter is omitted, and the one that was never passed in\nin the first place does not magically appear.\n\n==============\nBatch headings\n==============\n\nThe batched values are usually one kind of object such as bugs. The\nBatchNavigator's heading property contains a description of the objects\nfor display.\n\n >>> safe_reindeer_batch_navigator.heading\n 'results'\n\nThere is a special case for when there is only one item in the batch,\nthe singular version of the heading is returned.\n\n >>> navigator = BatchNavigator(['only-one'], request=request)\n >>> navigator.heading\n 'result'\n\n(Accessing .heading causes len() to be called on the underlying list,\nwhich is why we have to use the safe batch navigator. In theory, this\ncould be optimized, but there's no real point, since the heading is\ninvariably preceded by the actual length of the underlying list,\neg. \"10 results\". Since len() is called anyway, and its value is\ncached, a second len() won't hurt performance.)\n\nThe heading can be set by passing a singular and a plural version of\nthe heading. The batch navigation will return the appropriate\nheader based on the total items in the batch.\n\n >>> navigator = BatchNavigator(safe_reindeer, request=request)\n >>> navigator.setHeadings('bug', 'bugs')\n >>> navigator.heading\n 'bugs'\n\n >>> navigator = BatchNavigator(['only-one'], request=request)\n >>> navigator.setHeadings('bug', 'bugs')\n >>> navigator.heading\n 'bug'\n\n(Cleanup)\n\n >>> sm.unregisterAdapter(HTTPCharsets)\n True\n >>> sm.unregisterAdapter(ExampleAdapter)\n True\n\n===============\nOther Documents\n===============\n\n.. toctree::\n :glob:\n\n *\n docs/*\n\n============================\nNEWS for lazr.batchnavigator\n============================\n\n1.2.11 (2015-04-09)\n===================\n\n- Save a query if the slice is of the form [x:x].\n\n1.2.10 (2011-09-14)\n===================\n\n- delegate the calculation of the rough length of a result set to\n IRangeFactory.\n\n1.2.9 (2011-08-25)\n==================\n\n- When a backwards batch is at first too short and when another chunk\n from the result set is added, _Batch,sliced_list() does no longer\n use the memo value for the already retrived chunk.\n\n- don't use the parameter start to determine if a previous/next batch\n exists; don't rely on len(resultset) and to determine the real size\n of a batch.\n\n- Avoid negative start index on empty result sets.\n\n1.2.7 (2011-07-18)\n==================\n\n- retrieve slices of the result set in class _Batch only via methods\n of the range factory.\n\n1.2.6 (2011-07-28)\n==================\n\n- fixed an error in handling backwards batches which return less elements\n than expected.\n- URL-encode all query parameters in BatchNavigator.generateBatchURL()\n\n1.2.5 (2011-07-13)\n==================\n\n- Permit changing all variable names with a single prefix.\n\n1.2.4 (2011-04-11)\n==================\n\n- Permit overriding determineSize to control how the batch default and concrete\n sizes are determined in subclasses.\n- Listify (once we have sliced) rather than assuming batched slices will honour\n the complete list protocol.\n\n1.2.3 (2011-04-06)\n==================\n\n- Add IRangeFactory and the ability to use backend database hints for efficient\n retrieval of pages.\n\n- Remove terrible-scaling getBatchURLs method.\n\n1.2.2 (2010-08-19)\n==================\n\n- Make len() cheap to call when the current batch is the last (or\n only) batch.\n\n- Avoid calling len() when generating navigator URLs.\n\n1.2.1 (2010-08-12)\n==================\n\n- fix a bug in the len() of a batch when the batch had previously been\n iterated over\n\n1.2.0 (2010-08-05)\n==================\n\n- avoid calling len() on the underlying sequence when possible\n- return None for endNumber when the batch is out of range\n\n1.1.1 (2010-05-10)\n==================\n\n- Ignore negative batch sizes\n\n1.1 (2009-08-31)\n================\n\n- Remove build dependencies on bzr and egg_info\n\n- remove sys.path hack in setup.py for __version__\n\n1.0 (2009-03-24)\n================\n\n- Initial release on PyPI", "description_content_type": null, "docs_url": null, "download_url": "https://launchpad.net/lazr.batchnavigator/+download", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://launchpad.net/lazr.batchnavigator", "keywords": null, "license": "LGPL v3", "maintainer": null, "maintainer_email": null, "name": "lazr.batchnavigator", "package_url": "https://pypi.org/project/lazr.batchnavigator/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/lazr.batchnavigator/", "project_urls": { "Download": "https://launchpad.net/lazr.batchnavigator/+download", "Homepage": "https://launchpad.net/lazr.batchnavigator" }, "release_url": "https://pypi.org/project/lazr.batchnavigator/1.2.11/", "requires_dist": null, "requires_python": null, "summary": "A helper to navigate batched results in a web page.", "version": "1.2.11" }, "last_serial": 1497531, "releases": { "1.0": [ { "comment_text": "", "digests": { "md5": "2ebf09ce06e210e55001f768a9c99e82", "sha256": "93cf531140c50c679ebd96b6e07c1b02eddd535bf71bd75b1528137ff34b8ab4" }, "downloads": -1, "filename": "lazr.batchnavigator-1.0.tar.gz", "has_sig": false, "md5_digest": "2ebf09ce06e210e55001f768a9c99e82", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25633, "upload_time": "2009-03-26T17:40:12", "url": "https://files.pythonhosted.org/packages/69/e1/79923246ace2a5e380ad06a8d791ac6e899d39930064e2a28d0533d422d8/lazr.batchnavigator-1.0.tar.gz" } ], "1.1.1": [], "1.2.0": [], "1.2.1": [], "1.2.10": [], "1.2.11": [ { "comment_text": "", "digests": { "md5": "74eb49503ed5de8ebcff77168cf6ef57", "sha256": "71e4e5ec07970acf518448e53c7bafb0461ac8135b4635b86552ec2e3b2ea2d9" }, "downloads": -1, "filename": "lazr.batchnavigator-1.2.11.tar.gz", "has_sig": true, "md5_digest": "74eb49503ed5de8ebcff77168cf6ef57", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42038, "upload_time": "2015-04-09T13:51:45", "url": "https://files.pythonhosted.org/packages/fd/59/a25a5c6fb79fabf3bbd50c877f1600020c4b13793ef511994ad7a32ca854/lazr.batchnavigator-1.2.11.tar.gz" } ], "1.2.2": [], "1.2.5": [], "1.2.6": [], "1.2.7": [], "1.2.9": [] }, "urls": [ { "comment_text": "", "digests": { "md5": "74eb49503ed5de8ebcff77168cf6ef57", "sha256": "71e4e5ec07970acf518448e53c7bafb0461ac8135b4635b86552ec2e3b2ea2d9" }, "downloads": -1, "filename": "lazr.batchnavigator-1.2.11.tar.gz", "has_sig": true, "md5_digest": "74eb49503ed5de8ebcff77168cf6ef57", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42038, "upload_time": "2015-04-09T13:51:45", "url": "https://files.pythonhosted.org/packages/fd/59/a25a5c6fb79fabf3bbd50c877f1600020c4b13793ef511994ad7a32ca854/lazr.batchnavigator-1.2.11.tar.gz" } ] }